chore: conditionally dispatch network events (#18687)

This commit is contained in:
Pavel Feldman 2022-11-09 21:10:57 -08:00 committed by GitHub
parent cafa558845
commit c25e67a0e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 260 additions and 111 deletions

View file

@ -96,6 +96,13 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
this._channel.on('requestFinished', params => this._onRequestFinished(params)); this._channel.on('requestFinished', params => this._onRequestFinished(params));
this._channel.on('response', ({ response, page }) => this._onResponse(network.Response.from(response), Page.fromNullable(page))); this._channel.on('response', ({ response, page }) => this._onResponse(network.Response.from(response), Page.fromNullable(page)));
this._closedPromise = new Promise(f => this.once(Events.BrowserContext.Close, f)); this._closedPromise = new Promise(f => this.once(Events.BrowserContext.Close, f));
this._setEventToSubscriptionMapping(new Map<string, channels.BrowserContextUpdateSubscriptionParams['event']>([
[Events.BrowserContext.Request, 'request'],
[Events.BrowserContext.Response, 'response'],
[Events.BrowserContext.RequestFinished, 'requestFinished'],
[Events.BrowserContext.RequestFailed, 'requestFailed'],
]));
} }
_setBrowserType(browserType: BrowserType) { _setBrowserType(browserType: BrowserType) {

View file

@ -26,6 +26,8 @@ import type { ClientInstrumentation } from './clientInstrumentation';
import type { Connection } from './connection'; import type { Connection } from './connection';
import type { Logger } from './types'; import type { Logger } from './types';
type Listener = (...args: any[]) => void;
export abstract class ChannelOwner<T extends channels.Channel = channels.Channel> extends EventEmitter { export abstract class ChannelOwner<T extends channels.Channel = channels.Channel> extends EventEmitter {
readonly _connection: Connection; readonly _connection: Connection;
private _parent: ChannelOwner | undefined; private _parent: ChannelOwner | undefined;
@ -37,6 +39,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
readonly _initializer: channels.InitializerTraits<T>; readonly _initializer: channels.InitializerTraits<T>;
_logger: Logger | undefined; _logger: Logger | undefined;
_instrumentation: ClientInstrumentation | undefined; _instrumentation: ClientInstrumentation | undefined;
private _eventToSubscriptionMapping: Map<string, string> = new Map();
constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits<T>, instrumentation?: ClientInstrumentation) { constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits<T>, instrumentation?: ClientInstrumentation) {
super(); super();
@ -57,6 +60,51 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
this._initializer = initializer; this._initializer = initializer;
} }
_setEventToSubscriptionMapping(mapping: Map<string, string>) {
this._eventToSubscriptionMapping = mapping;
}
private _updateSubscription(event: string | symbol, enabled: boolean) {
const protocolEvent = this._eventToSubscriptionMapping.get(String(event));
if (protocolEvent)
(this._channel as any).updateSubscription({ event: protocolEvent, enabled }).catch(() => {});
}
override on(event: string | symbol, listener: Listener): this {
if (!this.listenerCount(event))
this._updateSubscription(event, true);
super.on(event, listener);
return this;
}
override addListener(event: string | symbol, listener: Listener): this {
if (!this.listenerCount(event))
this._updateSubscription(event, true);
super.addListener(event, listener);
return this;
}
override prependListener(event: string | symbol, listener: Listener): this {
if (!this.listenerCount(event))
this._updateSubscription(event, true);
super.prependListener(event, listener);
return this;
}
override off(event: string | symbol, listener: Listener): this {
super.off(event, listener);
if (!this.listenerCount(event))
this._updateSubscription(event, false);
return this;
}
override removeListener(event: string | symbol, listener: Listener): this {
super.removeListener(event, listener);
if (!this.listenerCount(event))
this._updateSubscription(event, false);
return this;
}
_adopt(child: ChannelOwner<any>) { _adopt(child: ChannelOwner<any>) {
child._parent!._objects.delete(child._guid); child._parent!._objects.delete(child._guid);
this._objects.set(child._guid, child); this._objects.set(child._guid, child);

View file

@ -66,7 +66,6 @@ type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> &
}, },
path?: string, path?: string,
}; };
type Listener = (...args: any[]) => void;
type ExpectScreenshotOptions = Omit<channels.PageExpectScreenshotOptions, 'screenshotOptions' | 'locator' | 'expected'> & { type ExpectScreenshotOptions = Omit<channels.PageExpectScreenshotOptions, 'screenshotOptions' | 'locator' | 'expected'> & {
expected?: Buffer, expected?: Buffer,
@ -161,6 +160,14 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
new Promise<void>(f => this.once(Events.Page.Close, f)), new Promise<void>(f => this.once(Events.Page.Close, f)),
new Promise<void>(f => this.once(Events.Page.Crash, f)), new Promise<void>(f => this.once(Events.Page.Crash, f)),
]); ]);
this._setEventToSubscriptionMapping(new Map<string, channels.PageUpdateSubscriptionParams['event']>([
[Events.Page.Request, 'request'],
[Events.Page.Response, 'response'],
[Events.Page.RequestFinished, 'requestFinished'],
[Events.Page.RequestFailed, 'requestFailed'],
[Events.Page.FileChooser, 'fileChooser'],
]));
} }
private _onFrameAttached(frame: Frame) { private _onFrameAttached(frame: Frame) {
@ -693,41 +700,6 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
return [...this._workers]; return [...this._workers];
} }
override on(event: string | symbol, listener: Listener): this {
if (event === Events.Page.FileChooser && !this.listenerCount(event))
this._channel.setFileChooserInterceptedNoReply({ intercepted: true });
super.on(event, listener);
return this;
}
override addListener(event: string | symbol, listener: Listener): this {
if (event === Events.Page.FileChooser && !this.listenerCount(event))
this._channel.setFileChooserInterceptedNoReply({ intercepted: true });
super.addListener(event, listener);
return this;
}
override prependListener(event: string | symbol, listener: Listener): this {
if (event === Events.Page.FileChooser && !this.listenerCount(event))
this._channel.setFileChooserInterceptedNoReply({ intercepted: true });
super.prependListener(event, listener);
return this;
}
override off(event: string | symbol, listener: Listener): this {
super.off(event, listener);
if (event === Events.Page.FileChooser && !this.listenerCount(event))
this._channel.setFileChooserInterceptedNoReply({ intercepted: false });
return this;
}
override removeListener(event: string | symbol, listener: Listener): this {
super.removeListener(event, listener);
if (event === Events.Page.FileChooser && !this.listenerCount(event))
this._channel.setFileChooserInterceptedNoReply({ intercepted: false });
return this;
}
async pause() { async pause() {
if (!require('inspector').url()) if (!require('inspector').url())
await this.context()._channel.pause(); await this.context()._channel.pause();

View file

@ -869,6 +869,11 @@ scheme.BrowserContextCreateTempFileParams = tObject({
scheme.BrowserContextCreateTempFileResult = tObject({ scheme.BrowserContextCreateTempFileResult = tObject({
writableStream: tChannel(['WritableStream']), writableStream: tChannel(['WritableStream']),
}); });
scheme.BrowserContextUpdateSubscriptionParams = tObject({
event: tEnum(['request', 'response', 'requestFinished', 'requestFailed']),
enabled: tBoolean,
});
scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({}));
scheme.PageInitializer = tObject({ scheme.PageInitializer = tObject({
mainFrame: tChannel(['Frame']), mainFrame: tChannel(['Frame']),
viewportSize: tOptional(tObject({ viewportSize: tOptional(tObject({
@ -927,10 +932,6 @@ scheme.PageSetDefaultTimeoutNoReplyParams = tObject({
timeout: tOptional(tNumber), timeout: tOptional(tNumber),
}); });
scheme.PageSetDefaultTimeoutNoReplyResult = tOptional(tObject({})); scheme.PageSetDefaultTimeoutNoReplyResult = tOptional(tObject({}));
scheme.PageSetFileChooserInterceptedNoReplyParams = tObject({
intercepted: tBoolean,
});
scheme.PageSetFileChooserInterceptedNoReplyResult = tOptional(tObject({}));
scheme.PageAddInitScriptParams = tObject({ scheme.PageAddInitScriptParams = tObject({
source: tString, source: tString,
}); });
@ -1162,6 +1163,11 @@ scheme.PageStopCSSCoverageResult = tObject({
}); });
scheme.PageBringToFrontParams = tOptional(tObject({})); scheme.PageBringToFrontParams = tOptional(tObject({}));
scheme.PageBringToFrontResult = tOptional(tObject({})); scheme.PageBringToFrontResult = tOptional(tObject({}));
scheme.PageUpdateSubscriptionParams = tObject({
event: tEnum(['fileChooser', 'request', 'response', 'requestFinished', 'requestFailed']),
enabled: tBoolean,
});
scheme.PageUpdateSubscriptionResult = tOptional(tObject({}));
scheme.FrameInitializer = tObject({ scheme.FrameInitializer = tObject({
url: tString, url: tString,
name: tString, name: tString,

View file

@ -15,7 +15,7 @@
*/ */
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { Dispatcher } from './dispatcher'; import { Dispatcher, existingDispatcher } from './dispatcher';
import type { DispatcherScope } from './dispatcher'; import type { DispatcherScope } from './dispatcher';
import { StreamDispatcher } from './streamDispatcher'; import { StreamDispatcher } from './streamDispatcher';
import fs from 'fs'; import fs from 'fs';
@ -24,7 +24,19 @@ import type { Artifact } from '../artifact';
export class ArtifactDispatcher extends Dispatcher<Artifact, channels.ArtifactChannel, DispatcherScope> implements channels.ArtifactChannel { export class ArtifactDispatcher extends Dispatcher<Artifact, channels.ArtifactChannel, DispatcherScope> implements channels.ArtifactChannel {
_type_Artifact = true; _type_Artifact = true;
constructor(scope: DispatcherScope, artifact: Artifact) {
static from(parentScope: DispatcherScope, artifact: Artifact): ArtifactDispatcher {
return ArtifactDispatcher.fromNullable(parentScope, artifact)!;
}
static fromNullable(parentScope: DispatcherScope, artifact: Artifact): ArtifactDispatcher | undefined {
if (!artifact)
return undefined;
const result = existingDispatcher<ArtifactDispatcher>(artifact);
return result || new ArtifactDispatcher(parentScope, artifact);
}
private constructor(scope: DispatcherScope, artifact: Artifact) {
super(scope, artifact, 'Artifact', { super(scope, artifact, 'Artifact', {
absolutePath: artifact.localPath(), absolutePath: artifact.localPath(),
}); });

View file

@ -15,7 +15,7 @@
*/ */
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
import { Dispatcher, lookupDispatcher } from './dispatcher'; import { Dispatcher, existingDispatcher } from './dispatcher';
import type { DispatcherScope } from './dispatcher'; import type { DispatcherScope } from './dispatcher';
import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher'; import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher';
import type { FrameDispatcher } from './frameDispatcher'; import type { FrameDispatcher } from './frameDispatcher';
@ -38,6 +38,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
_type_EventTarget = true; _type_EventTarget = true;
_type_BrowserContext = true; _type_BrowserContext = true;
private _context: BrowserContext; private _context: BrowserContext;
private _subscriptions = new Set<channels.BrowserContextUpdateSubscriptionParams['event']>();
constructor(parentScope: DispatcherScope, context: BrowserContext) { constructor(parentScope: DispatcherScope, context: BrowserContext) {
// We will reparent these to the context below. // We will reparent these to the context below.
@ -60,7 +61,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
const onVideo = (artifact: Artifact) => { const onVideo = (artifact: Artifact) => {
// Note: Video must outlive Page and BrowserContext, so that client can saveAs it // Note: Video must outlive Page and BrowserContext, so that client can saveAs it
// after closing the context. We use |scope| for it. // after closing the context. We use |scope| for it.
const artifactDispatcher = new ArtifactDispatcher(parentScope, artifact); const artifactDispatcher = ArtifactDispatcher.from(parentScope, artifact);
this._dispatchEvent('video', { artifact: artifactDispatcher }); this._dispatchEvent('video', { artifact: artifactDispatcher });
}; };
this.addObjectListener(BrowserContext.Events.VideoStarted, onVideo); this.addObjectListener(BrowserContext.Events.VideoStarted, onVideo);
@ -88,27 +89,56 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
this.addObjectListener(CRBrowserContext.CREvents.ServiceWorker, serviceWorker => this._dispatchEvent('serviceWorker', { worker: new WorkerDispatcher(this, serviceWorker) })); this.addObjectListener(CRBrowserContext.CREvents.ServiceWorker, serviceWorker => this._dispatchEvent('serviceWorker', { worker: new WorkerDispatcher(this, serviceWorker) }));
} }
this.addObjectListener(BrowserContext.Events.Request, (request: Request) => { this.addObjectListener(BrowserContext.Events.Request, (request: Request) => {
return this._dispatchEvent('request', { const redirectFromDispatcher = request.redirectedFrom() && existingDispatcher(request.redirectedFrom());
request: RequestDispatcher.from(this, request), if (!redirectFromDispatcher && !this._shouldDispatchNetworkEvent(request, 'request'))
return;
const requestDispatcher = RequestDispatcher.from(this, request);
this._dispatchEvent('request', {
request: requestDispatcher,
page: PageDispatcher.fromNullable(this, request.frame()?._page.initializedOrUndefined()) page: PageDispatcher.fromNullable(this, request.frame()?._page.initializedOrUndefined())
}); });
}); });
this.addObjectListener(BrowserContext.Events.Response, (response: Response) => this._dispatchEvent('response', { this.addObjectListener(BrowserContext.Events.Response, (response: Response) => {
response: ResponseDispatcher.from(this, response), const requestDispatcher = existingDispatcher<RequestDispatcher>(response.request());
page: PageDispatcher.fromNullable(this, response.frame()?._page.initializedOrUndefined()) if (!requestDispatcher && !this._shouldDispatchNetworkEvent(response.request(), 'response'))
})); return;
this.addObjectListener(BrowserContext.Events.RequestFailed, (request: Request) => this._dispatchEvent('requestFailed', { this._dispatchEvent('response', {
request: RequestDispatcher.from(this, request), response: ResponseDispatcher.from(this, response),
failureText: request._failureText || undefined, page: PageDispatcher.fromNullable(this, response.frame()?._page.initializedOrUndefined())
responseEndTiming: request._responseEndTiming, });
page: PageDispatcher.fromNullable(this, request.frame()?._page.initializedOrUndefined()) });
})); this.addObjectListener(BrowserContext.Events.RequestFailed, (request: Request) => {
this.addObjectListener(BrowserContext.Events.RequestFinished, ({ request, response }: { request: Request, response: Response | null }) => this._dispatchEvent('requestFinished', { const requestDispatcher = existingDispatcher<RequestDispatcher>(request);
request: RequestDispatcher.from(this, request), if (!requestDispatcher && !this._shouldDispatchNetworkEvent(request, 'requestFailed'))
response: ResponseDispatcher.fromNullable(this, response), return;
responseEndTiming: request._responseEndTiming, this._dispatchEvent('requestFailed', {
page: PageDispatcher.fromNullable(this, request.frame()?._page.initializedOrUndefined()), request: RequestDispatcher.from(this, request),
})); failureText: request._failureText || undefined,
responseEndTiming: request._responseEndTiming,
page: PageDispatcher.fromNullable(this, request.frame()?._page.initializedOrUndefined())
});
});
this.addObjectListener(BrowserContext.Events.RequestFinished, ({ request, response }: { request: Request, response: Response | null }) => {
const requestDispatcher = existingDispatcher<RequestDispatcher>(request);
if (!requestDispatcher && !this._shouldDispatchNetworkEvent(request, 'requestFinished'))
return;
this._dispatchEvent('requestFinished', {
request: RequestDispatcher.from(this, request),
response: ResponseDispatcher.fromNullable(this, response),
responseEndTiming: request._responseEndTiming,
page: PageDispatcher.fromNullable(this, request.frame()?._page.initializedOrUndefined()),
});
});
}
private _shouldDispatchNetworkEvent(request: Request, event: channels.BrowserContextUpdateSubscriptionParams['event'] & channels.PageUpdateSubscriptionParams['event']): boolean {
if (this._subscriptions.has(event))
return true;
const page = request.frame()?._page?.initializedOrUndefined();
const pageDispatcher = page ? existingDispatcher<PageDispatcher>(page) : undefined;
if (pageDispatcher?._subscriptions.has(event))
return true;
return false;
} }
async createTempFile(params: channels.BrowserContextCreateTempFileParams, metadata?: channels.Metadata): Promise<channels.BrowserContextCreateTempFileResult> { async createTempFile(params: channels.BrowserContextCreateTempFileParams, metadata?: channels.Metadata): Promise<channels.BrowserContextCreateTempFileResult> {
@ -138,7 +168,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
} }
async newPage(params: channels.BrowserContextNewPageParams, metadata: CallMetadata): Promise<channels.BrowserContextNewPageResult> { async newPage(params: channels.BrowserContextNewPageParams, metadata: CallMetadata): Promise<channels.BrowserContextNewPageResult> {
return { page: lookupDispatcher<PageDispatcher>(await this._context.newPage(metadata)) }; return { page: PageDispatcher.from(this, await this._context.newPage(metadata)) };
} }
async cookies(params: channels.BrowserContextCookiesParams): Promise<channels.BrowserContextCookiesResult> { async cookies(params: channels.BrowserContextCookiesParams): Promise<channels.BrowserContextCookiesResult> {
@ -225,7 +255,14 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
const artifact = await this._context._harExport(params.harId); const artifact = await this._context._harExport(params.harId);
if (!artifact) if (!artifact)
throw new Error('No HAR artifact. Ensure record.harPath is set.'); throw new Error('No HAR artifact. Ensure record.harPath is set.');
return { artifact: new ArtifactDispatcher(this, artifact) }; return { artifact: ArtifactDispatcher.from(this, artifact) };
}
async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams, metadata?: channels.Metadata | undefined): Promise<void> {
if (params.enabled)
this._subscriptions.add(params.event);
else
this._subscriptions.delete(params.event);
} }
override _dispose() { override _dispose() {

View file

@ -18,7 +18,7 @@ import { EventEmitter } from 'events';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { serializeError } from '../../protocol/serializers'; import { serializeError } from '../../protocol/serializers';
import { findValidator, ValidationError, createMetadataValidator, type ValidatorContext } from '../../protocol/validator'; import { findValidator, ValidationError, createMetadataValidator, type ValidatorContext } from '../../protocol/validator';
import { assert, debugAssert, isUnderTest, monotonicTime } from '../../utils'; import { assert, isUnderTest, monotonicTime } from '../../utils';
import { kBrowserOrContextClosedError } from '../../common/errors'; import { kBrowserOrContextClosedError } from '../../common/errors';
import type { CallMetadata } from '../instrumentation'; import type { CallMetadata } from '../instrumentation';
import { SdkObject } from '../instrumentation'; import { SdkObject } from '../instrumentation';
@ -30,20 +30,10 @@ import type { RegisteredListener } from '../..//utils/eventsHelper';
export const dispatcherSymbol = Symbol('dispatcher'); export const dispatcherSymbol = Symbol('dispatcher');
const metadataValidator = createMetadataValidator(); const metadataValidator = createMetadataValidator();
export function lookupDispatcher<DispatcherType>(object: any): DispatcherType { export function existingDispatcher<DispatcherType>(object: any): DispatcherType | undefined {
const result = object[dispatcherSymbol];
debugAssert(result);
return result;
}
export function existingDispatcher<DispatcherType>(object: any): DispatcherType {
return object[dispatcherSymbol]; return object[dispatcherSymbol];
} }
export function lookupNullableDispatcher<DispatcherType>(object: any | null): DispatcherType | undefined {
return object ? lookupDispatcher(object) : undefined;
}
export class Dispatcher<Type extends { guid: string }, ChannelType, ParentScopeType extends DispatcherScope> extends EventEmitter implements channels.Channel { export class Dispatcher<Type extends { guid: string }, ChannelType, ParentScopeType extends DispatcherScope> extends EventEmitter implements channels.Channel {
private _connection: DispatcherConnection; private _connection: DispatcherConnection;
// Parent is always "isScope". // Parent is always "isScope".

View file

@ -18,14 +18,15 @@ import type { ElementHandle } from '../dom';
import type { Frame } from '../frames'; import type { Frame } from '../frames';
import type * as js from '../javascript'; import type * as js from '../javascript';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { existingDispatcher, lookupNullableDispatcher } from './dispatcher'; import { existingDispatcher } from './dispatcher';
import { JSHandleDispatcher, serializeResult, parseArgument } from './jsHandleDispatcher'; import { JSHandleDispatcher, serializeResult, parseArgument } from './jsHandleDispatcher';
import type { JSHandleDispatcherParentScope } from './jsHandleDispatcher'; import type { JSHandleDispatcherParentScope } from './jsHandleDispatcher';
import type { FrameDispatcher } from './frameDispatcher'; import { FrameDispatcher } from './frameDispatcher';
import type { CallMetadata } from '../instrumentation'; import type { CallMetadata } from '../instrumentation';
import type { WritableStreamDispatcher } from './writableStreamDispatcher'; import type { WritableStreamDispatcher } from './writableStreamDispatcher';
import { assert } from '../../utils'; import { assert } from '../../utils';
import path from 'path'; import path from 'path';
import type { PageDispatcher } from './pageDispatcher';
export class ElementHandleDispatcher extends JSHandleDispatcher implements channels.ElementHandleChannel { export class ElementHandleDispatcher extends JSHandleDispatcher implements channels.ElementHandleChannel {
_type_ElementHandle = true; _type_ElementHandle = true;
@ -55,11 +56,13 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann
} }
async ownerFrame(params: channels.ElementHandleOwnerFrameParams, metadata: CallMetadata): Promise<channels.ElementHandleOwnerFrameResult> { async ownerFrame(params: channels.ElementHandleOwnerFrameParams, metadata: CallMetadata): Promise<channels.ElementHandleOwnerFrameResult> {
return { frame: lookupNullableDispatcher<FrameDispatcher>(await this._elementHandle.ownerFrame()) }; const frame = await this._elementHandle.ownerFrame();
return { frame: frame ? FrameDispatcher.from(this.parentScope() as PageDispatcher, frame) : undefined };
} }
async contentFrame(params: channels.ElementHandleContentFrameParams, metadata: CallMetadata): Promise<channels.ElementHandleContentFrameResult> { async contentFrame(params: channels.ElementHandleContentFrameParams, metadata: CallMetadata): Promise<channels.ElementHandleContentFrameResult> {
return { frame: lookupNullableDispatcher<FrameDispatcher>(await this._elementHandle.contentFrame()) }; const frame = await this._elementHandle.contentFrame();
return { frame: frame ? FrameDispatcher.from(this.parentScope() as PageDispatcher, frame) : undefined };
} }
async getAttribute(params: channels.ElementHandleGetAttributeParams, metadata: CallMetadata): Promise<channels.ElementHandleGetAttributeResult> { async getAttribute(params: channels.ElementHandleGetAttributeParams, metadata: CallMetadata): Promise<channels.ElementHandleGetAttributeResult> {

View file

@ -17,10 +17,10 @@
import type { NavigationEvent } from '../frames'; import type { NavigationEvent } from '../frames';
import { Frame } from '../frames'; import { Frame } from '../frames';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { Dispatcher, lookupNullableDispatcher, existingDispatcher } from './dispatcher'; import { Dispatcher, existingDispatcher } from './dispatcher';
import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { ElementHandleDispatcher } from './elementHandlerDispatcher';
import { parseArgument, serializeResult } from './jsHandleDispatcher'; import { parseArgument, serializeResult } from './jsHandleDispatcher';
import type { ResponseDispatcher } from './networkDispatchers'; import { ResponseDispatcher } from './networkDispatchers';
import { RequestDispatcher } from './networkDispatchers'; import { RequestDispatcher } from './networkDispatchers';
import type { CallMetadata } from '../instrumentation'; import type { CallMetadata } from '../instrumentation';
import type { WritableStreamDispatcher } from './writableStreamDispatcher'; import type { WritableStreamDispatcher } from './writableStreamDispatcher';
@ -62,13 +62,13 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Pa
return; return;
const params = { url: event.url, name: event.name, error: event.error ? event.error.message : undefined }; const params = { url: event.url, name: event.name, error: event.error ? event.error.message : undefined };
if (event.newDocument) if (event.newDocument)
(params as any).newDocument = { request: RequestDispatcher.fromNullable(scope.parentScope(), event.newDocument.request || null) }; (params as any).newDocument = { request: RequestDispatcher.fromNullable(this.parentScope().parentScope(), event.newDocument.request || null) };
this._dispatchEvent('navigated', params); this._dispatchEvent('navigated', params);
}); });
} }
async goto(params: channels.FrameGotoParams, metadata: CallMetadata): Promise<channels.FrameGotoResult> { async goto(params: channels.FrameGotoParams, metadata: CallMetadata): Promise<channels.FrameGotoResult> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._frame.goto(metadata, params.url, params)) }; return { response: ResponseDispatcher.fromNullable(this.parentScope().parentScope(), await this._frame.goto(metadata, params.url, params)) };
} }
async frameElement(): Promise<channels.FrameFrameElementResult> { async frameElement(): Promise<channels.FrameFrameElementResult> {

View file

@ -20,7 +20,7 @@ import type { CallMetadata } from '../instrumentation';
import type { Request, Response, Route } from '../network'; import type { Request, Response, Route } from '../network';
import { WebSocket } from '../network'; import { WebSocket } from '../network';
import type { RootDispatcher } from './dispatcher'; import type { RootDispatcher } from './dispatcher';
import { Dispatcher, existingDispatcher, lookupNullableDispatcher } from './dispatcher'; import { Dispatcher, existingDispatcher } from './dispatcher';
import { TracingDispatcher } from './tracingDispatcher'; import { TracingDispatcher } from './tracingDispatcher';
import type { BrowserContextDispatcher } from './browserContextDispatcher'; import type { BrowserContextDispatcher } from './browserContextDispatcher';
import type { PageDispatcher } from './pageDispatcher'; import type { PageDispatcher } from './pageDispatcher';
@ -60,7 +60,7 @@ export class RequestDispatcher extends Dispatcher<Request, channels.RequestChann
} }
async response(): Promise<channels.RequestResponseResult> { async response(): Promise<channels.RequestResponseResult> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._object.response()) }; return { response: ResponseDispatcher.fromNullable(this.parentScope(), await this._object.response()) };
} }
} }

View file

@ -18,13 +18,13 @@ import type { BrowserContext } from '../browserContext';
import type { Frame } from '../frames'; import type { Frame } from '../frames';
import { Page, Worker } from '../page'; import { Page, Worker } from '../page';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { Dispatcher, existingDispatcher, lookupDispatcher, lookupNullableDispatcher } from './dispatcher'; import { Dispatcher, existingDispatcher } from './dispatcher';
import { parseError, serializeError } from '../../protocol/serializers'; import { parseError, serializeError } from '../../protocol/serializers';
import { ConsoleMessageDispatcher } from './consoleMessageDispatcher'; import { ConsoleMessageDispatcher } from './consoleMessageDispatcher';
import { DialogDispatcher } from './dialogDispatcher'; import { DialogDispatcher } from './dialogDispatcher';
import { FrameDispatcher } from './frameDispatcher'; import { FrameDispatcher } from './frameDispatcher';
import { RequestDispatcher } from './networkDispatchers'; import { RequestDispatcher } from './networkDispatchers';
import type { ResponseDispatcher } from './networkDispatchers'; import { ResponseDispatcher } from './networkDispatchers';
import { RouteDispatcher, WebSocketDispatcher } from './networkDispatchers'; import { RouteDispatcher, WebSocketDispatcher } from './networkDispatchers';
import { serializeResult, parseArgument } from './jsHandleDispatcher'; import { serializeResult, parseArgument } from './jsHandleDispatcher';
import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { ElementHandleDispatcher } from './elementHandlerDispatcher';
@ -42,6 +42,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
_type_EventTarget = true; _type_EventTarget = true;
_type_Page = true; _type_Page = true;
private _page: Page; private _page: Page;
_subscriptions = new Set<channels.PageUpdateSubscriptionParams['event']>();
static from(parentScope: BrowserContextDispatcher, page: Page): PageDispatcher { static from(parentScope: BrowserContextDispatcher, page: Page): PageDispatcher {
return PageDispatcher.fromNullable(parentScope, page)!; return PageDispatcher.fromNullable(parentScope, page)!;
@ -80,7 +81,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
this.addObjectListener(Page.Events.Dialog, dialog => this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this, dialog) })); this.addObjectListener(Page.Events.Dialog, dialog => this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this, dialog) }));
this.addObjectListener(Page.Events.Download, (download: Download) => { this.addObjectListener(Page.Events.Download, (download: Download) => {
// Artifact can outlive the page, so bind to the context scope. // Artifact can outlive the page, so bind to the context scope.
this._dispatchEvent('download', { url: download.url, suggestedFilename: download.suggestedFilename(), artifact: new ArtifactDispatcher(parentScope, download.artifact) }); this._dispatchEvent('download', { url: download.url, suggestedFilename: download.suggestedFilename(), artifact: ArtifactDispatcher.from(parentScope, download.artifact) });
}); });
this.addObjectListener(Page.Events.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', { this.addObjectListener(Page.Events.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', {
element: ElementHandleDispatcher.from(this, fileChooser.element()), element: ElementHandleDispatcher.from(this, fileChooser.element()),
@ -91,9 +92,9 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
this.addObjectListener(Page.Events.PageError, error => this._dispatchEvent('pageError', { error: serializeError(error) })); this.addObjectListener(Page.Events.PageError, error => this._dispatchEvent('pageError', { error: serializeError(error) }));
this.addObjectListener(Page.Events.WebSocket, webSocket => this._dispatchEvent('webSocket', { webSocket: new WebSocketDispatcher(this, webSocket) })); this.addObjectListener(Page.Events.WebSocket, webSocket => this._dispatchEvent('webSocket', { webSocket: new WebSocketDispatcher(this, webSocket) }));
this.addObjectListener(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this, worker) })); this.addObjectListener(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this, worker) }));
this.addObjectListener(Page.Events.Video, (artifact: Artifact) => this._dispatchEvent('video', { artifact: existingDispatcher<ArtifactDispatcher>(artifact) })); this.addObjectListener(Page.Events.Video, (artifact: Artifact) => this._dispatchEvent('video', { artifact: ArtifactDispatcher.from(parentScope, artifact) }));
if (page._video) if (page._video)
this._dispatchEvent('video', { artifact: existingDispatcher<ArtifactDispatcher>(page._video) }); this._dispatchEvent('video', { artifact: ArtifactDispatcher.from(this.parentScope(), page._video) });
// Ensure client knows about all frames. // Ensure client knows about all frames.
const frames = page._frameManager.frames(); const frames = page._frameManager.frames();
for (let i = 1; i < frames.length; i++) for (let i = 1; i < frames.length; i++)
@ -125,15 +126,15 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
} }
async reload(params: channels.PageReloadParams, metadata: CallMetadata): Promise<channels.PageReloadResult> { async reload(params: channels.PageReloadParams, metadata: CallMetadata): Promise<channels.PageReloadResult> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.reload(metadata, params)) }; return { response: ResponseDispatcher.fromNullable(this.parentScope(), await this._page.reload(metadata, params)) };
} }
async goBack(params: channels.PageGoBackParams, metadata: CallMetadata): Promise<channels.PageGoBackResult> { async goBack(params: channels.PageGoBackParams, metadata: CallMetadata): Promise<channels.PageGoBackResult> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goBack(metadata, params)) }; return { response: ResponseDispatcher.fromNullable(this.parentScope(), await this._page.goBack(metadata, params)) };
} }
async goForward(params: channels.PageGoForwardParams, metadata: CallMetadata): Promise<channels.PageGoForwardResult> { async goForward(params: channels.PageGoForwardParams, metadata: CallMetadata): Promise<channels.PageGoForwardResult> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goForward(metadata, params)) }; return { response: ResponseDispatcher.fromNullable(this.parentScope(), await this._page.goForward(metadata, params)) };
} }
async emulateMedia(params: channels.PageEmulateMediaParams, metadata: CallMetadata): Promise<void> { async emulateMedia(params: channels.PageEmulateMediaParams, metadata: CallMetadata): Promise<void> {
@ -194,8 +195,13 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
await this._page.close(metadata, params); await this._page.close(metadata, params);
} }
async setFileChooserInterceptedNoReply(params: channels.PageSetFileChooserInterceptedNoReplyParams, metadata: CallMetadata): Promise<void> { async updateSubscription(params: channels.PageUpdateSubscriptionParams, metadata?: channels.Metadata | undefined): Promise<void> {
await this._page.setFileChooserIntercepted(params.intercepted); if (params.event === 'fileChooser')
await this._page.setFileChooserIntercepted(params.enabled);
if (params.enabled)
this._subscriptions.add(params.event);
else
this._subscriptions.delete(params.event);
} }
async keyboardDown(params: channels.PageKeyboardDownParams, metadata: CallMetadata): Promise<void> { async keyboardDown(params: channels.PageKeyboardDownParams, metadata: CallMetadata): Promise<void> {
@ -286,7 +292,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
} }
_onFrameDetached(frame: Frame) { _onFrameDetached(frame: Frame) {
this._dispatchEvent('frameDetached', { frame: lookupDispatcher<FrameDispatcher>(frame) }); this._dispatchEvent('frameDetached', { frame: FrameDispatcher.from(this, frame) });
} }
override _dispose() { override _dispose() {
@ -330,7 +336,7 @@ export class BindingCallDispatcher extends Dispatcher<{ guid: string }, channels
constructor(scope: PageDispatcher, name: string, needsHandle: boolean, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) { constructor(scope: PageDispatcher, name: string, needsHandle: boolean, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) {
super(scope, { guid: 'bindingCall@' + createGuid() }, 'BindingCall', { super(scope, { guid: 'bindingCall@' + createGuid() }, 'BindingCall', {
frame: lookupDispatcher<FrameDispatcher>(source.frame), frame: FrameDispatcher.from(scope, source.frame),
name, name,
args: needsHandle ? undefined : args.map(serializeResult), args: needsHandle ? undefined : args.map(serializeResult),
handle: needsHandle ? ElementHandleDispatcher.fromJSHandle(scope, args[0] as JSHandle) : undefined, handle: needsHandle ? ElementHandleDispatcher.fromJSHandle(scope, args[0] as JSHandle) : undefined,

View file

@ -43,7 +43,7 @@ export class TracingDispatcher extends Dispatcher<Tracing, channels.TracingChann
async tracingStopChunk(params: channels.TracingTracingStopChunkParams): Promise<channels.TracingTracingStopChunkResult> { async tracingStopChunk(params: channels.TracingTracingStopChunkParams): Promise<channels.TracingTracingStopChunkResult> {
const { artifact, sourceEntries } = await this._object.stopChunk(params); const { artifact, sourceEntries } = await this._object.stopChunk(params);
return { artifact: artifact ? new ArtifactDispatcher(this, artifact) : undefined, sourceEntries }; return { artifact: artifact ? ArtifactDispatcher.from(this, artifact) : undefined, sourceEntries };
} }
async tracingStop(params: channels.TracingTracingStopParams): Promise<channels.TracingTracingStopResult> { async tracingStop(params: channels.TracingTracingStopParams): Promise<channels.TracingTracingStopResult> {

View file

@ -1369,6 +1369,7 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
harStart(params: BrowserContextHarStartParams, metadata?: Metadata): Promise<BrowserContextHarStartResult>; harStart(params: BrowserContextHarStartParams, metadata?: Metadata): Promise<BrowserContextHarStartResult>;
harExport(params: BrowserContextHarExportParams, metadata?: Metadata): Promise<BrowserContextHarExportResult>; harExport(params: BrowserContextHarExportParams, metadata?: Metadata): Promise<BrowserContextHarExportResult>;
createTempFile(params: BrowserContextCreateTempFileParams, metadata?: Metadata): Promise<BrowserContextCreateTempFileResult>; createTempFile(params: BrowserContextCreateTempFileParams, metadata?: Metadata): Promise<BrowserContextCreateTempFileResult>;
updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: Metadata): Promise<BrowserContextUpdateSubscriptionResult>;
} }
export type BrowserContextBindingCallEvent = { export type BrowserContextBindingCallEvent = {
binding: BindingCallChannel, binding: BindingCallChannel,
@ -1598,6 +1599,14 @@ export type BrowserContextCreateTempFileOptions = {
export type BrowserContextCreateTempFileResult = { export type BrowserContextCreateTempFileResult = {
writableStream: WritableStreamChannel, writableStream: WritableStreamChannel,
}; };
export type BrowserContextUpdateSubscriptionParams = {
event: 'request' | 'response' | 'requestFinished' | 'requestFailed',
enabled: boolean,
};
export type BrowserContextUpdateSubscriptionOptions = {
};
export type BrowserContextUpdateSubscriptionResult = void;
export interface BrowserContextEvents { export interface BrowserContextEvents {
'bindingCall': BrowserContextBindingCallEvent; 'bindingCall': BrowserContextBindingCallEvent;
@ -1643,7 +1652,6 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
_type_Page: boolean; _type_Page: boolean;
setDefaultNavigationTimeoutNoReply(params: PageSetDefaultNavigationTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultNavigationTimeoutNoReplyResult>; setDefaultNavigationTimeoutNoReply(params: PageSetDefaultNavigationTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultNavigationTimeoutNoReplyResult>;
setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultTimeoutNoReplyResult>; setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultTimeoutNoReplyResult>;
setFileChooserInterceptedNoReply(params: PageSetFileChooserInterceptedNoReplyParams, metadata?: Metadata): Promise<PageSetFileChooserInterceptedNoReplyResult>;
addInitScript(params: PageAddInitScriptParams, metadata?: Metadata): Promise<PageAddInitScriptResult>; addInitScript(params: PageAddInitScriptParams, metadata?: Metadata): Promise<PageAddInitScriptResult>;
close(params: PageCloseParams, metadata?: Metadata): Promise<PageCloseResult>; close(params: PageCloseParams, metadata?: Metadata): Promise<PageCloseResult>;
emulateMedia(params: PageEmulateMediaParams, metadata?: Metadata): Promise<PageEmulateMediaResult>; emulateMedia(params: PageEmulateMediaParams, metadata?: Metadata): Promise<PageEmulateMediaResult>;
@ -1674,6 +1682,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
startCSSCoverage(params: PageStartCSSCoverageParams, metadata?: Metadata): Promise<PageStartCSSCoverageResult>; startCSSCoverage(params: PageStartCSSCoverageParams, metadata?: Metadata): Promise<PageStartCSSCoverageResult>;
stopCSSCoverage(params?: PageStopCSSCoverageParams, metadata?: Metadata): Promise<PageStopCSSCoverageResult>; stopCSSCoverage(params?: PageStopCSSCoverageParams, metadata?: Metadata): Promise<PageStopCSSCoverageResult>;
bringToFront(params?: PageBringToFrontParams, metadata?: Metadata): Promise<PageBringToFrontResult>; bringToFront(params?: PageBringToFrontParams, metadata?: Metadata): Promise<PageBringToFrontResult>;
updateSubscription(params: PageUpdateSubscriptionParams, metadata?: Metadata): Promise<PageUpdateSubscriptionResult>;
} }
export type PageBindingCallEvent = { export type PageBindingCallEvent = {
binding: BindingCallChannel, binding: BindingCallChannel,
@ -1730,13 +1739,6 @@ export type PageSetDefaultTimeoutNoReplyOptions = {
timeout?: number, timeout?: number,
}; };
export type PageSetDefaultTimeoutNoReplyResult = void; export type PageSetDefaultTimeoutNoReplyResult = void;
export type PageSetFileChooserInterceptedNoReplyParams = {
intercepted: boolean,
};
export type PageSetFileChooserInterceptedNoReplyOptions = {
};
export type PageSetFileChooserInterceptedNoReplyResult = void;
export type PageAddInitScriptParams = { export type PageAddInitScriptParams = {
source: string, source: string,
}; };
@ -2114,6 +2116,14 @@ export type PageStopCSSCoverageResult = {
export type PageBringToFrontParams = {}; export type PageBringToFrontParams = {};
export type PageBringToFrontOptions = {}; export type PageBringToFrontOptions = {};
export type PageBringToFrontResult = void; export type PageBringToFrontResult = void;
export type PageUpdateSubscriptionParams = {
event: 'fileChooser' | 'request' | 'response' | 'requestFinished' | 'requestFailed',
enabled: boolean,
};
export type PageUpdateSubscriptionOptions = {
};
export type PageUpdateSubscriptionResult = void;
export interface PageEvents { export interface PageEvents {
'bindingCall': PageBindingCallEvent; 'bindingCall': PageBindingCallEvent;

View file

@ -1090,6 +1090,17 @@ BrowserContext:
returns: returns:
writableStream: WritableStream writableStream: WritableStream
updateSubscription:
parameters:
event:
type: enum
literals:
- request
- response
- requestFinished
- requestFailed
enabled: boolean
events: events:
bindingCall: bindingCall:
@ -1167,10 +1178,6 @@ Page:
parameters: parameters:
timeout: number? timeout: number?
setFileChooserInterceptedNoReply:
parameters:
intercepted: boolean
addInitScript: addInitScript:
parameters: parameters:
source: string source: string
@ -1486,6 +1493,18 @@ Page:
bringToFront: bringToFront:
updateSubscription:
parameters:
event:
type: enum
literals:
- fileChooser
- request
- response
- requestFinished
- requestFailed
enabled: boolean
events: events:
bindingCall: bindingCall:

View file

@ -183,6 +183,45 @@ it('should scope browser handles', async ({ browserType, expectScopeState }) =>
expectScopeState(browserType, GOLDEN_PRECONDITION); expectScopeState(browserType, GOLDEN_PRECONDITION);
}); });
it('should not generate dispatchers for subresources w/o listeners', async ({ page, server, browserType, expectScopeState }) => {
server.setRedirect('/one-style.css', '/two-style.css');
server.setRedirect('/two-style.css', '/three-style.css');
server.setRedirect('/three-style.css', '/four-style.css');
server.setRoute('/four-style.css', (req, res) => res.end('body {box-sizing: border-box; }'));
await page.goto(server.PREFIX + '/one-style.html');
expectScopeState(browserType, {
_guid: '',
objects: [
{ _guid: 'android', objects: [] },
{ _guid: 'browser-type', objects: [] },
{ _guid: 'browser-type', objects: [] },
{ _guid: 'browser-type', objects: [
{
_guid: 'browser', objects: [
{ _guid: 'browser-context', objects: [
{
_guid: 'page', objects: [
{ _guid: 'frame', objects: [] }
]
},
{ _guid: 'request', objects: [] },
{ _guid: 'request-context', objects: [] },
{ _guid: 'response', objects: [] },
{ _guid: 'tracing', objects: [] }
] },
]
}],
},
{ _guid: 'electron', objects: [] },
{ _guid: 'localUtils', objects: [] },
{ _guid: 'Playwright', objects: [] },
{ _guid: 'selectors', objects: [] },
]
});
});
it('should work with the domain module', async ({ browserType, server, browserName }) => { it('should work with the domain module', async ({ browserType, server, browserName }) => {
const local = domain.create(); const local = domain.create();
local.run(() => { }); local.run(() => { });