diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 2f2ffbb35d..f39f74406a 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -96,6 +96,13 @@ export class BrowserContext extends ChannelOwner 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([ + [Events.BrowserContext.Request, 'request'], + [Events.BrowserContext.Response, 'response'], + [Events.BrowserContext.RequestFinished, 'requestFinished'], + [Events.BrowserContext.RequestFailed, 'requestFailed'], + ])); } _setBrowserType(browserType: BrowserType) { diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index c2ebf7ada8..0fe9ffd0ce 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -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 extends EventEmitter { readonly _connection: Connection; private _parent: ChannelOwner | undefined; @@ -37,6 +39,7 @@ export abstract class ChannelOwner; _logger: Logger | undefined; _instrumentation: ClientInstrumentation | undefined; + private _eventToSubscriptionMapping: Map = new Map(); constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits, instrumentation?: ClientInstrumentation) { super(); @@ -57,6 +60,51 @@ export abstract class ChannelOwner) { + 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) { child._parent!._objects.delete(child._guid); this._objects.set(child._guid, child); diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index a89f61f47f..a2fbc09f20 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -66,7 +66,6 @@ type PDFOptions = Omit & }, path?: string, }; -type Listener = (...args: any[]) => void; type ExpectScreenshotOptions = Omit & { expected?: Buffer, @@ -161,6 +160,14 @@ export class Page extends ChannelOwner implements api.Page new Promise(f => this.once(Events.Page.Close, f)), new Promise(f => this.once(Events.Page.Crash, f)), ]); + + this._setEventToSubscriptionMapping(new Map([ + [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 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(); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 9619294dcf..85b34a34d2 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -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, diff --git a/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts b/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts index 0d3a3324eb..6d954f75f6 100644 --- a/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts @@ -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 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(artifact); + return result || new ArtifactDispatcher(parentScope, artifact); + } + + private constructor(scope: DispatcherScope, artifact: Artifact) { super(scope, artifact, 'Artifact', { absolutePath: artifact.localPath(), }); diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index ca72fd11b5..915ffb0a46 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -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(); constructor(parentScope: DispatcherScope, context: BrowserContext) { // We will reparent these to the context below. @@ -60,7 +61,7 @@ export class BrowserContextDispatcher extends Dispatcher { // 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 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(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(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(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(page) : undefined; + if (pageDispatcher?._subscriptions.has(event)) + return true; + return false; } async createTempFile(params: channels.BrowserContextCreateTempFileParams, metadata?: channels.Metadata): Promise { @@ -138,7 +168,7 @@ export class BrowserContextDispatcher extends Dispatcher { - return { page: lookupDispatcher(await this._context.newPage(metadata)) }; + return { page: PageDispatcher.from(this, await this._context.newPage(metadata)) }; } async cookies(params: channels.BrowserContextCookiesParams): Promise { @@ -225,7 +255,14 @@ export class BrowserContextDispatcher extends Dispatcher { + if (params.enabled) + this._subscriptions.add(params.event); + else + this._subscriptions.delete(params.event); } override _dispose() { diff --git a/packages/playwright-core/src/server/dispatchers/dispatcher.ts b/packages/playwright-core/src/server/dispatchers/dispatcher.ts index d8ba8b7242..7f689383a1 100644 --- a/packages/playwright-core/src/server/dispatchers/dispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/dispatcher.ts @@ -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(object: any): DispatcherType { - const result = object[dispatcherSymbol]; - debugAssert(result); - return result; -} - -export function existingDispatcher(object: any): DispatcherType { +export function existingDispatcher(object: any): DispatcherType | undefined { return object[dispatcherSymbol]; } -export function lookupNullableDispatcher(object: any | null): DispatcherType | undefined { - return object ? lookupDispatcher(object) : undefined; -} - export class Dispatcher extends EventEmitter implements channels.Channel { private _connection: DispatcherConnection; // Parent is always "isScope". diff --git a/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts b/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts index 49206cb18c..cda8d4a999 100644 --- a/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts @@ -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 { - return { frame: lookupNullableDispatcher(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 { - return { frame: lookupNullableDispatcher(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 { diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index 413249e08d..0646133dcc 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -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 { - return { response: lookupNullableDispatcher(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 { diff --git a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts index fd3eb2bd7f..5405643bf3 100644 --- a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts +++ b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts @@ -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 { - return { response: lookupNullableDispatcher(await this._object.response()) }; + return { response: ResponseDispatcher.fromNullable(this.parentScope(), await this._object.response()) }; } } diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 651920e38d..f7cbd1db2f 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -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(); static from(parentScope: BrowserContextDispatcher, page: Page): PageDispatcher { return PageDispatcher.fromNullable(parentScope, page)!; @@ -80,7 +81,7 @@ export class PageDispatcher extends Dispatcher 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 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(artifact) })); + this.addObjectListener(Page.Events.Video, (artifact: Artifact) => this._dispatchEvent('video', { artifact: ArtifactDispatcher.from(parentScope, artifact) })); if (page._video) - this._dispatchEvent('video', { artifact: existingDispatcher(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 { - return { response: lookupNullableDispatcher(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 { - return { response: lookupNullableDispatcher(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 { - return { response: lookupNullableDispatcher(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 { @@ -194,8 +195,13 @@ export class PageDispatcher extends Dispatcher { - await this._page.setFileChooserIntercepted(params.intercepted); + async updateSubscription(params: channels.PageUpdateSubscriptionParams, metadata?: channels.Metadata | undefined): Promise { + 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 { @@ -286,7 +292,7 @@ export class PageDispatcher extends Dispatcher(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(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, diff --git a/packages/playwright-core/src/server/dispatchers/tracingDispatcher.ts b/packages/playwright-core/src/server/dispatchers/tracingDispatcher.ts index fd6d752c8d..d0b6d7654f 100644 --- a/packages/playwright-core/src/server/dispatchers/tracingDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/tracingDispatcher.ts @@ -43,7 +43,7 @@ export class TracingDispatcher extends Dispatcher { 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 { diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index aade6ed415..59412f4d50 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1369,6 +1369,7 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT harStart(params: BrowserContextHarStartParams, metadata?: Metadata): Promise; harExport(params: BrowserContextHarExportParams, metadata?: Metadata): Promise; createTempFile(params: BrowserContextCreateTempFileParams, metadata?: Metadata): Promise; + updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: Metadata): Promise; } 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; setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise; - setFileChooserInterceptedNoReply(params: PageSetFileChooserInterceptedNoReplyParams, metadata?: Metadata): Promise; addInitScript(params: PageAddInitScriptParams, metadata?: Metadata): Promise; close(params: PageCloseParams, metadata?: Metadata): Promise; emulateMedia(params: PageEmulateMediaParams, metadata?: Metadata): Promise; @@ -1674,6 +1682,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel { startCSSCoverage(params: PageStartCSSCoverageParams, metadata?: Metadata): Promise; stopCSSCoverage(params?: PageStopCSSCoverageParams, metadata?: Metadata): Promise; bringToFront(params?: PageBringToFrontParams, metadata?: Metadata): Promise; + updateSubscription(params: PageUpdateSubscriptionParams, metadata?: Metadata): Promise; } 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; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 1626e94fd1..45515d2d98 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -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: diff --git a/tests/library/channels.spec.ts b/tests/library/channels.spec.ts index 0e85787a83..2a4132b214 100644 --- a/tests/library/channels.spec.ts +++ b/tests/library/channels.spec.ts @@ -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(() => { });