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('response', ({ response, page }) => this._onResponse(network.Response.from(response), Page.fromNullable(page)));
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) {

View file

@ -26,6 +26,8 @@ import type { ClientInstrumentation } from './clientInstrumentation';
import type { Connection } from './connection';
import type { Logger } from './types';
type Listener = (...args: any[]) => void;
export abstract class ChannelOwner<T extends channels.Channel = channels.Channel> extends EventEmitter {
readonly _connection: Connection;
private _parent: ChannelOwner | undefined;
@ -37,6 +39,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
readonly _initializer: channels.InitializerTraits<T>;
_logger: Logger | 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) {
super();
@ -57,6 +60,51 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
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>) {
child._parent!._objects.delete(child._guid);
this._objects.set(child._guid, child);

View file

@ -66,7 +66,6 @@ type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> &
},
path?: string,
};
type Listener = (...args: any[]) => void;
type ExpectScreenshotOptions = Omit<channels.PageExpectScreenshotOptions, 'screenshotOptions' | 'locator' | 'expected'> & {
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.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) {
@ -693,41 +700,6 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
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() {
if (!require('inspector').url())
await this.context()._channel.pause();

View file

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

View file

@ -15,7 +15,7 @@
*/
import type * as channels from '@protocol/channels';
import { Dispatcher } from './dispatcher';
import { Dispatcher, existingDispatcher } from './dispatcher';
import type { DispatcherScope } from './dispatcher';
import { StreamDispatcher } from './streamDispatcher';
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 {
_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', {
absolutePath: artifact.localPath(),
});

View file

@ -15,7 +15,7 @@
*/
import { BrowserContext } from '../browserContext';
import { Dispatcher, lookupDispatcher } from './dispatcher';
import { Dispatcher, existingDispatcher } from './dispatcher';
import type { DispatcherScope } from './dispatcher';
import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher';
import type { FrameDispatcher } from './frameDispatcher';
@ -38,6 +38,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
_type_EventTarget = true;
_type_BrowserContext = true;
private _context: BrowserContext;
private _subscriptions = new Set<channels.BrowserContextUpdateSubscriptionParams['event']>();
constructor(parentScope: DispatcherScope, context: BrowserContext) {
// We will reparent these to the context below.
@ -60,7 +61,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
const onVideo = (artifact: Artifact) => {
// Note: Video must outlive Page and BrowserContext, so that client can saveAs 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.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(BrowserContext.Events.Request, (request: Request) => {
return this._dispatchEvent('request', {
request: RequestDispatcher.from(this, request),
const redirectFromDispatcher = request.redirectedFrom() && existingDispatcher(request.redirectedFrom());
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())
});
});
this.addObjectListener(BrowserContext.Events.Response, (response: Response) => this._dispatchEvent('response', {
response: ResponseDispatcher.from(this, response),
page: PageDispatcher.fromNullable(this, response.frame()?._page.initializedOrUndefined())
}));
this.addObjectListener(BrowserContext.Events.RequestFailed, (request: Request) => this._dispatchEvent('requestFailed', {
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 }) => this._dispatchEvent('requestFinished', {
request: RequestDispatcher.from(this, request),
response: ResponseDispatcher.fromNullable(this, response),
responseEndTiming: request._responseEndTiming,
page: PageDispatcher.fromNullable(this, request.frame()?._page.initializedOrUndefined()),
}));
this.addObjectListener(BrowserContext.Events.Response, (response: Response) => {
const requestDispatcher = existingDispatcher<RequestDispatcher>(response.request());
if (!requestDispatcher && !this._shouldDispatchNetworkEvent(response.request(), 'response'))
return;
this._dispatchEvent('response', {
response: ResponseDispatcher.from(this, response),
page: PageDispatcher.fromNullable(this, response.frame()?._page.initializedOrUndefined())
});
});
this.addObjectListener(BrowserContext.Events.RequestFailed, (request: Request) => {
const requestDispatcher = existingDispatcher<RequestDispatcher>(request);
if (!requestDispatcher && !this._shouldDispatchNetworkEvent(request, 'requestFailed'))
return;
this._dispatchEvent('requestFailed', {
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> {
@ -138,7 +168,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
}
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> {
@ -225,7 +255,14 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
const artifact = await this._context._harExport(params.harId);
if (!artifact)
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() {

View file

@ -18,7 +18,7 @@ import { EventEmitter } from 'events';
import type * as channels from '@protocol/channels';
import { serializeError } from '../../protocol/serializers';
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 type { CallMetadata } from '../instrumentation';
import { SdkObject } from '../instrumentation';
@ -30,20 +30,10 @@ import type { RegisteredListener } from '../..//utils/eventsHelper';
export const dispatcherSymbol = Symbol('dispatcher');
const metadataValidator = createMetadataValidator();
export function lookupDispatcher<DispatcherType>(object: any): DispatcherType {
const result = object[dispatcherSymbol];
debugAssert(result);
return result;
}
export function existingDispatcher<DispatcherType>(object: any): DispatcherType {
export function existingDispatcher<DispatcherType>(object: any): DispatcherType | undefined {
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 {
private _connection: DispatcherConnection;
// Parent is always "isScope".

View file

@ -18,14 +18,15 @@ import type { ElementHandle } from '../dom';
import type { Frame } from '../frames';
import type * as js from '../javascript';
import type * as channels from '@protocol/channels';
import { existingDispatcher, lookupNullableDispatcher } from './dispatcher';
import { existingDispatcher } from './dispatcher';
import { JSHandleDispatcher, serializeResult, parseArgument } from './jsHandleDispatcher';
import type { JSHandleDispatcherParentScope } from './jsHandleDispatcher';
import type { FrameDispatcher } from './frameDispatcher';
import { FrameDispatcher } from './frameDispatcher';
import type { CallMetadata } from '../instrumentation';
import type { WritableStreamDispatcher } from './writableStreamDispatcher';
import { assert } from '../../utils';
import path from 'path';
import type { PageDispatcher } from './pageDispatcher';
export class ElementHandleDispatcher extends JSHandleDispatcher implements channels.ElementHandleChannel {
_type_ElementHandle = true;
@ -55,11 +56,13 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann
}
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> {
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> {

View file

@ -17,10 +17,10 @@
import type { NavigationEvent } from '../frames';
import { Frame } from '../frames';
import type * as channels from '@protocol/channels';
import { Dispatcher, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
import { Dispatcher, existingDispatcher } from './dispatcher';
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
import { parseArgument, serializeResult } from './jsHandleDispatcher';
import type { ResponseDispatcher } from './networkDispatchers';
import { ResponseDispatcher } from './networkDispatchers';
import { RequestDispatcher } from './networkDispatchers';
import type { CallMetadata } from '../instrumentation';
import type { WritableStreamDispatcher } from './writableStreamDispatcher';
@ -62,13 +62,13 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Pa
return;
const params = { url: event.url, name: event.name, error: event.error ? event.error.message : undefined };
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);
});
}
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> {

View file

@ -20,7 +20,7 @@ import type { CallMetadata } from '../instrumentation';
import type { Request, Response, Route } from '../network';
import { WebSocket } from '../network';
import type { RootDispatcher } from './dispatcher';
import { Dispatcher, existingDispatcher, lookupNullableDispatcher } from './dispatcher';
import { Dispatcher, existingDispatcher } from './dispatcher';
import { TracingDispatcher } from './tracingDispatcher';
import type { BrowserContextDispatcher } from './browserContextDispatcher';
import type { PageDispatcher } from './pageDispatcher';
@ -60,7 +60,7 @@ export class RequestDispatcher extends Dispatcher<Request, channels.RequestChann
}
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 { Page, Worker } from '../page';
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 { ConsoleMessageDispatcher } from './consoleMessageDispatcher';
import { DialogDispatcher } from './dialogDispatcher';
import { FrameDispatcher } from './frameDispatcher';
import { RequestDispatcher } from './networkDispatchers';
import type { ResponseDispatcher } from './networkDispatchers';
import { ResponseDispatcher } from './networkDispatchers';
import { RouteDispatcher, WebSocketDispatcher } from './networkDispatchers';
import { serializeResult, parseArgument } from './jsHandleDispatcher';
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
@ -42,6 +42,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
_type_EventTarget = true;
_type_Page = true;
private _page: Page;
_subscriptions = new Set<channels.PageUpdateSubscriptionParams['event']>();
static from(parentScope: BrowserContextDispatcher, page: Page): PageDispatcher {
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.Download, (download: Download) => {
// 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', {
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.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.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)
this._dispatchEvent('video', { artifact: existingDispatcher<ArtifactDispatcher>(page._video) });
this._dispatchEvent('video', { artifact: ArtifactDispatcher.from(this.parentScope(), page._video) });
// Ensure client knows about all frames.
const frames = page._frameManager.frames();
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> {
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> {
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> {
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> {
@ -194,8 +195,13 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
await this._page.close(metadata, params);
}
async setFileChooserInterceptedNoReply(params: channels.PageSetFileChooserInterceptedNoReplyParams, metadata: CallMetadata): Promise<void> {
await this._page.setFileChooserIntercepted(params.intercepted);
async updateSubscription(params: channels.PageUpdateSubscriptionParams, metadata?: channels.Metadata | undefined): Promise<void> {
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> {
@ -286,7 +292,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
}
_onFrameDetached(frame: Frame) {
this._dispatchEvent('frameDetached', { frame: lookupDispatcher<FrameDispatcher>(frame) });
this._dispatchEvent('frameDetached', { frame: FrameDispatcher.from(this, frame) });
}
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[]) {
super(scope, { guid: 'bindingCall@' + createGuid() }, 'BindingCall', {
frame: lookupDispatcher<FrameDispatcher>(source.frame),
frame: FrameDispatcher.from(scope, source.frame),
name,
args: needsHandle ? undefined : args.map(serializeResult),
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> {
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> {

View file

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

View file

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

View file

@ -183,6 +183,45 @@ it('should scope browser handles', async ({ browserType, expectScopeState }) =>
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 }) => {
const local = domain.create();
local.run(() => { });