diff --git a/packages/playwright-core/src/common/socksProxy.ts b/packages/playwright-core/src/common/socksProxy.ts index 6566262d64..e72d96350e 100644 --- a/packages/playwright-core/src/common/socksProxy.ts +++ b/packages/playwright-core/src/common/socksProxy.ts @@ -19,7 +19,7 @@ import type { AddressInfo } from 'net'; import net from 'net'; import { debugLogger } from '../utils/debugLogger'; import { createSocket } from '../utils/happy-eyeballs'; -import { assert, createGuid, } from '../utils'; +import { assert, createGuid, ManagedEventEmitter, } from '../utils'; // https://tools.ietf.org/html/rfc1928 @@ -366,13 +366,11 @@ export function parsePattern(pattern: string | undefined): PatternMatcher { return (host, port) => matchers.some(matcher => matcher(host, port)); } -export class SocksProxy extends EventEmitter implements SocksConnectionClient { - static Events = { - SocksRequested: 'socksRequested', - SocksData: 'socksData', - SocksClosed: 'socksClosed', - }; - +export class SocksProxy extends ManagedEventEmitter<{ + 'socksRequested': [SocksSocketRequestedPayload], + 'socksData': [SocksSocketDataPayload], + 'socksClosed': [SocksSocketClosedPayload], +}> implements SocksConnectionClient { private _server: net.Server; private _connections = new Map(); private _sockets = new Set(); @@ -456,7 +454,7 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient { this._handleDirect(payload); return; } - this.emit(SocksProxy.Events.SocksRequested, payload); + this.emit('socksRequested', payload); } onSocketData(payload: SocksSocketDataPayload): void { @@ -465,7 +463,7 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient { direct.write(payload.data); return; } - this.emit(SocksProxy.Events.SocksData, payload); + this.emit('socksData', payload); } onSocketClosed(payload: SocksSocketClosedPayload): void { @@ -475,7 +473,7 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient { this._directSockets.delete(payload.uid); return; } - this.emit(SocksProxy.Events.SocksClosed, payload); + this.emit('socksClosed', payload); } socketConnected({ uid, host, port }: SocksSocketConnectedPayload) { @@ -499,15 +497,13 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient { } } -export class SocksProxyHandler extends EventEmitter { - static Events = { - SocksConnected: 'socksConnected', - SocksData: 'socksData', - SocksError: 'socksError', - SocksFailed: 'socksFailed', - SocksEnd: 'socksEnd', - }; - +export class SocksProxyHandler extends EventEmitter<{ + socksConnected: [SocksSocketConnectedPayload], + socksData: [SocksSocketDataPayload], + socksError: [SocksSocketErrorPayload], + socksFailed: [SocksSocketFailedPayload], + socksEnd: [SocksSocketEndPayload], +}> { private _sockets = new Map(); private _patternMatcher: PatternMatcher = () => false; private _redirectPortForTest: number | undefined; @@ -528,7 +524,7 @@ export class SocksProxyHandler extends EventEmitter { if (!this._patternMatcher(host, port)) { const payload: SocksSocketFailedPayload = { uid, errorCode: 'ERULESET' }; debugLogger.log('socks', `[${uid}] <= pattern error ${payload.errorCode}`); - this.emit(SocksProxyHandler.Events.SocksFailed, payload); + this.emit('socksFailed', payload); return; } @@ -540,18 +536,18 @@ export class SocksProxyHandler extends EventEmitter { const socket = await createSocket(host, port); socket.on('data', data => { const payload: SocksSocketDataPayload = { uid, data }; - this.emit(SocksProxyHandler.Events.SocksData, payload); + this.emit('socksData', payload); }); socket.on('error', error => { const payload: SocksSocketErrorPayload = { uid, error: error.message }; debugLogger.log('socks', `[${uid}] <= network socket error ${payload.error}`); - this.emit(SocksProxyHandler.Events.SocksError, payload); + this.emit('socksError', payload); this._sockets.delete(uid); }); socket.on('end', () => { const payload: SocksSocketEndPayload = { uid }; debugLogger.log('socks', `[${uid}] <= network socket closed`); - this.emit(SocksProxyHandler.Events.SocksEnd, payload); + this.emit('socksEnd', payload); this._sockets.delete(uid); }); const localAddress = socket.localAddress; @@ -559,11 +555,11 @@ export class SocksProxyHandler extends EventEmitter { this._sockets.set(uid, socket); const payload: SocksSocketConnectedPayload = { uid, host: localAddress!, port: localPort! }; debugLogger.log('socks', `[${uid}] <= connected to network ${payload.host}:${payload.port}`); - this.emit(SocksProxyHandler.Events.SocksConnected, payload); + this.emit('socksConnected', payload); } catch (error) { const payload: SocksSocketFailedPayload = { uid, errorCode: error.code }; debugLogger.log('socks', `[${uid}] <= connect error ${payload.errorCode}`); - this.emit(SocksProxyHandler.Events.SocksFailed, payload); + this.emit('socksFailed', payload); } } diff --git a/packages/playwright-core/src/remote/playwrightConnection.ts b/packages/playwright-core/src/remote/playwrightConnection.ts index cce31207a8..b7d13afafc 100644 --- a/packages/playwright-core/src/remote/playwrightConnection.ts +++ b/packages/playwright-core/src/remote/playwrightConnection.ts @@ -18,12 +18,12 @@ import type { WebSocket } from '../utilsBundle'; import type { DispatcherScope, Playwright } from '../server'; import type * as channels from '@protocol/channels'; import { createPlaywright, DispatcherConnection, RootDispatcher, PlaywrightDispatcher } from '../server'; -import { Browser } from '../server/browser'; +import type { Browser } from '../server/browser'; import { serverSideCallMetadata } from '../server/instrumentation'; import { SocksProxy } from '../common/socksProxy'; import { assert, isUnderTest } from '../utils'; import type { LaunchOptions } from '../server/types'; -import { AndroidDevice } from '../server/android/android'; +import type { AndroidDevice } from '../server/android/android'; import { DebugControllerDispatcher } from '../server/dispatchers/debugControllerDispatcher'; import { startProfiling, stopProfiling } from '../utils'; import { monotonicTime } from '../utils'; @@ -123,7 +123,7 @@ export class PlaywrightConnection { for (const browser of playwright.allBrowsers()) await browser.close({ reason: 'Connection terminated' }); }); - browser.on(Browser.Events.Disconnected, () => { + browser.on('disconnected', () => { // Underlying browser did close for some reason - force disconnect the client. this.close({ code: 1001, reason: 'Browser closed' }); }); @@ -139,7 +139,7 @@ export class PlaywrightConnection { this._preLaunched.socksProxy?.setPattern(this._options.socksProxyPattern); const browser = this._preLaunched.browser!; - browser.on(Browser.Events.Disconnected, () => { + browser.on('disconnected', () => { // Underlying browser did close for some reason - force disconnect the client. this.close({ code: 1001, reason: 'Browser closed' }); }); @@ -158,7 +158,7 @@ export class PlaywrightConnection { debugLogger.log('server', `[${this._id}] engaged pre-launched (Android) mode`); const playwright = this._preLaunched.playwright!; const androidDevice = this._preLaunched.androidDevice!; - androidDevice.on(AndroidDevice.Events.Close, () => { + androidDevice.on('close', () => { // Underlying browser did close for some reason - force disconnect the client. this.close({ code: 1001, reason: 'Android device disconnected' }); }); @@ -202,7 +202,7 @@ export class PlaywrightConnection { ...this._options.launchOptions, headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS, }); - browser.on(Browser.Events.Disconnected, () => { + browser.on('disconnected', () => { // Underlying browser did close for some reason - force disconnect the client. this.close({ code: 1001, reason: 'Browser closed' }); }); diff --git a/packages/playwright-core/src/server/android/android.ts b/packages/playwright-core/src/server/android/android.ts index 0b4cb331b0..9b674b4804 100644 --- a/packages/playwright-core/src/server/android/android.ts +++ b/packages/playwright-core/src/server/android/android.ts @@ -21,6 +21,7 @@ import os from 'os'; import path from 'path'; import type * as stream from 'stream'; import { wsReceiver, wsSender } from '../../utilsBundle'; +import type { ManagedEventEmitter } from '../../utils'; import { createGuid, makeWaitForNextTask, isUnderTest, getPackageManagerExecCommand } from '../../utils'; import { removeFolders } from '../../utils/fileUtils'; import type { BrowserOptions, BrowserProcess } from '../browser'; @@ -53,7 +54,13 @@ export interface DeviceBackend { open(command: string): Promise; } -export interface SocketBackend extends EventEmitter { +export type SocketBackendEvents = { + data: [Buffer], + close: [] + error: [Error] +}; + +export interface SocketBackend extends ManagedEventEmitter { guid: string; write(data: Buffer): Promise; close(): void; @@ -96,7 +103,11 @@ export class Android extends SdkObject { } } -export class AndroidDevice extends SdkObject { +export class AndroidDevice extends SdkObject<{ + webViewAdded: [channels.AndroidWebView] + webViewRemoved: [string] + close: [] +}> { readonly _backend: DeviceBackend; readonly model: string; readonly serial: string; @@ -108,12 +119,6 @@ export class AndroidDevice extends SdkObject { readonly _timeoutSettings: TimeoutSettings; private _webViews = new Map(); - static Events = { - WebViewAdded: 'webViewAdded', - WebViewRemoved: 'webViewRemoved', - Close: 'close', - }; - private _browserConnections = new Set(); readonly _android: Android; private _isClosed = false; @@ -256,7 +261,7 @@ export class AndroidDevice extends SdkObject { } await this._backend.close(); this._android._deviceClosed(this); - this.emit(AndroidDevice.Events.Close); + this.emit('close'); } async launchBrowser(pkg: string = 'com.android.chrome', options: channels.AndroidDeviceLaunchBrowserParams): Promise { @@ -420,12 +425,12 @@ export class AndroidDevice extends SdkObject { const webView = { pid, pkg, socketName }; this._webViews.set(socketName, webView); - this.emit(AndroidDevice.Events.WebViewAdded, webView); + this.emit('webViewAdded', webView); } for (const p of this._webViews.keys()) { if (!socketNames.has(p)) { this._webViews.delete(p); - this.emit(AndroidDevice.Events.WebViewRemoved, p); + this.emit('webViewRemoved', p); } } } diff --git a/packages/playwright-core/src/server/android/backendAdb.ts b/packages/playwright-core/src/server/android/backendAdb.ts index cd718b9d95..aaa44852ae 100644 --- a/packages/playwright-core/src/server/android/backendAdb.ts +++ b/packages/playwright-core/src/server/android/backendAdb.ts @@ -17,9 +17,8 @@ import { debug } from '../../utilsBundle'; import type * as channels from '@protocol/channels'; import * as net from 'net'; -import { EventEmitter } from 'events'; -import type { Backend, DeviceBackend, SocketBackend } from './android'; -import { assert, createGuid } from '../../utils'; +import type { Backend, DeviceBackend, SocketBackend, SocketBackendEvents } from './android'; +import { assert, createGuid, ManagedEventEmitter } from '../../utils'; export class AdbBackend implements Backend { async devices(options: channels.AndroidDevicesOptions = {}): Promise { @@ -112,7 +111,7 @@ function encodeMessage(message: string): Buffer { return Buffer.from(lenHex + message); } -class BufferedSocketWrapper extends EventEmitter implements SocketBackend { +class BufferedSocketWrapper extends ManagedEventEmitter implements SocketBackend { readonly guid = createGuid(); private _socket: net.Socket; private _buffer = Buffer.from([]); diff --git a/packages/playwright-core/src/server/browser.ts b/packages/playwright-core/src/server/browser.ts index c37325b485..721fcc0c54 100644 --- a/packages/playwright-core/src/server/browser.ts +++ b/packages/playwright-core/src/server/browser.ts @@ -52,13 +52,10 @@ export type BrowserOptions = { originalLaunchOptions: types.LaunchOptions; }; -export abstract class Browser extends SdkObject { - - static Events = { - BeforeClose: 'beforeClose', - Disconnected: 'disconnected', - }; - +export abstract class Browser extends SdkObject<{ + beforeClose: [] + disconnected: [] +}> { readonly options: BrowserOptions; private _downloads = new Map(); _defaultContext: BrowserContext | null = null; @@ -140,8 +137,8 @@ export abstract class Browser extends SdkObject { pageOrError.then(page => { if (page instanceof Page) { page._video = artifact; - page.emitOnContext(BrowserContext.Events.VideoStarted, artifact); - page.emit(Page.Events.Video, artifact); + page.emitOnContext('videostarted', artifact); + page.emit('video', artifact); } }); } @@ -158,7 +155,7 @@ export abstract class Browser extends SdkObject { _didClose() { if (this._needsBeforeCloseEvent) - this.emit(Browser.Events.BeforeClose); + this.emit('beforeClose'); else this.beforeCloseFinished(); } @@ -168,7 +165,7 @@ export abstract class Browser extends SdkObject { context._browserClosed(); if (this._defaultContext) this._defaultContext._browserClosed(); - this.emit(Browser.Events.Disconnected); + this.emit('disconnected'); this.instrumentation.onBrowserClose(this); } @@ -180,7 +177,7 @@ export abstract class Browser extends SdkObject { await this.options.browserProcess.close(); } if (this.isConnected()) - await new Promise(x => this.once(Browser.Events.Disconnected, x)); + await new Promise(x => this.once('disconnected', x)); } async killForTests() { diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index b67771fe9a..1e018c09db 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -25,7 +25,7 @@ import type * as frames from './frames'; import { helper } from './helper'; import * as network from './network'; import { InitScript } from './page'; -import type { PageDelegate } from './page'; +import type { PageDelegate, Worker } from './page'; import { Page, PageBinding } from './page'; import type { Progress, ProgressController } from './progress'; import type { Selectors } from './selectors'; @@ -44,27 +44,33 @@ import { BrowserContextAPIRequestContext } from './fetch'; import type { Artifact } from './artifact'; import { Clock } from './clock'; import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; +import type { ConsoleMessage } from './console'; +import type { Dialog } from './dialog'; -export abstract class BrowserContext extends SdkObject { - static Events = { - Console: 'console', - Close: 'close', - Dialog: 'dialog', - Page: 'page', - // Can't use just 'error' due to node.js special treatment of error events. - // @see https://nodejs.org/api/events.html#events_error_events - PageError: 'pageerror', - Request: 'request', - Response: 'response', - RequestFailed: 'requestfailed', - RequestFinished: 'requestfinished', - RequestAborted: 'requestaborted', - RequestFulfilled: 'requestfulfilled', - RequestContinued: 'requestcontinued', - BeforeClose: 'beforeclose', - VideoStarted: 'videostarted', - }; +export type BrowserContextEvents = { + console: [ConsoleMessage] + close: [] + dialog: [Dialog] + page: [Page] + // Can't use just 'error' due to node.js special treatment of error events. + // @see https://nodejs.org/api/events.html#events_error_events + pageerror: [Error, Page] + request: [network.Request] + response: [network.Response] + requestfailed: [network.Request] + requestfinished: [{ request: network.Request, response: network.Response | null}] + requestaborted: [network.Request] + requestfulfilled: [network.Request] + requestcontinued: [network.Request] + beforeclose: [] + videostarted: [Artifact] + // Chromium only + backgroundpage: [Page] + serviceworker: [Worker] +}; + +export abstract class BrowserContext extends SdkObject { readonly _timeoutSettings = new TimeoutSettings(); readonly _pageBindings = new Map(); readonly _activeProgressControllers = new Set(); @@ -136,7 +142,7 @@ export abstract class BrowserContext extends SdkObject { // When paused, show inspector. if (this._debugger.isPaused()) Recorder.showInspector(this); - this._debugger.on(Debugger.Events.PausedStateChanged, () => { + this._debugger.on('pausedstatechanged', () => { Recorder.showInspector(this); }); @@ -252,7 +258,7 @@ export abstract class BrowserContext extends SdkObject { if (this._isPersistentContext) this.onClosePersistent(); this._closePromiseFulfill!(new Error('Context closed')); - this.emit(BrowserContext.Events.Close); + this.emit('close'); } // BrowserContext methods. @@ -359,7 +365,7 @@ export abstract class BrowserContext extends SdkObject { async _loadDefaultContextAsIs(progress: Progress): Promise { if (!this.pages().length) { - const waitForEvent = helper.waitForEvent(progress, this, BrowserContext.Events.Page); + const waitForEvent = helper.waitForEvent(progress, this, 'page'); progress.cleanupWhenAborted(() => waitForEvent.dispose); const page = (await waitForEvent.promise) as Page; if (page._pageIsError) @@ -443,7 +449,7 @@ export abstract class BrowserContext extends SdkObject { if (this._closedStatus === 'open') { if (options.reason) this._closeReason = options.reason; - this.emit(BrowserContext.Events.BeforeClose); + this.emit('beforeclose'); this._closedStatus = 'closing'; for (const harRecorder of this._harRecorders.values()) @@ -615,10 +621,10 @@ export abstract class BrowserContext extends SdkObject { async extendInjectedScript(source: string, arg?: any) { const installInFrame = (frame: frames.Frame) => frame.extendInjectedScript(source, arg).catch(() => {}); const installInPage = (page: Page) => { - page.on(Page.Events.InternalFrameNavigatedToNewDocument, installInFrame); + page.on('internalframenavigatedtonewdocument', installInFrame); return Promise.all(page.frames().map(installInFrame)); }; - this.on(BrowserContext.Events.Page, installInPage); + this.on('page', installInPage); return Promise.all(this.pages().map(installInPage)); } diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index e745cca214..fd20b15ec9 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -28,7 +28,6 @@ import type { ConnectionTransport, ProtocolRequest } from '../transport'; import { WebSocketTransport } from '../transport'; import { CRDevTools } from './crDevTools'; import type { BrowserOptions, BrowserProcess } from '../browser'; -import { Browser } from '../browser'; import type * as types from '../types'; import type * as channels from '@protocol/channels'; import type { HTTPRequestParams } from '../../utils/network'; @@ -121,7 +120,7 @@ export class Chromium extends BrowserType { progress.throwIfAborted(); const browser = await CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions); browser._isCollocatedWithServer = false; - browser.on(Browser.Events.Disconnected, doCleanup); + browser.on('disconnected', doCleanup); return browser; } diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index 777ff2eee8..0069fe5818 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -192,7 +192,7 @@ export class CRBrowser extends Browser { if (targetInfo.type === 'service_worker') { const serviceWorker = new CRServiceWorker(context, session, targetInfo.url); this._serviceWorkers.set(targetInfo.targetId, serviceWorker); - context.emit(CRBrowserContext.CREvents.ServiceWorker, serviceWorker); + context.emit('serviceworker', serviceWorker); return; } @@ -333,11 +333,6 @@ export class CRBrowser extends Browser { } export class CRBrowserContext extends BrowserContext { - static CREvents = { - BackgroundPage: 'backgroundpage', - ServiceWorker: 'serviceworker', - }; - declare readonly _browser: CRBrowser; constructor(browser: CRBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) { diff --git a/packages/playwright-core/src/server/chromium/crConnection.ts b/packages/playwright-core/src/server/chromium/crConnection.ts index c257d0d7ec..46a9bca49b 100644 --- a/packages/playwright-core/src/server/chromium/crConnection.ts +++ b/packages/playwright-core/src/server/chromium/crConnection.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { type RegisteredListener, assert, eventsHelper } from '../../utils'; +import { ManagedEventEmitter, type RegisteredListener, assert, eventsHelper } from '../../utils'; import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import type { Protocol } from './protocol'; import { EventEmitter } from 'events'; @@ -97,7 +97,7 @@ export class CRConnection extends EventEmitter { type SessionEventListener = (method: string, params?: Object) => void; -export class CRSession extends EventEmitter { +export class CRSession extends ManagedEventEmitter<{ [K in keyof Protocol.Events]: [Protocol.Events[K]] }> { private readonly _connection: CRConnection; private _eventListener?: SessionEventListener; private readonly _callbacks = new Map void, reject: (e: ProtocolError) => void, error: ProtocolError }>(); @@ -105,11 +105,6 @@ export class CRSession extends EventEmitter { private readonly _parentSession: CRSession | null; private _crashed: boolean = false; private _closed = false; - override on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override removeListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; constructor(connection: CRConnection, parentSession: CRSession | null, sessionId: string, eventListener?: SessionEventListener) { super(); @@ -169,6 +164,7 @@ export class CRSession extends EventEmitter { Promise.resolve().then(() => { if (this._eventListener) this._eventListener(object.method!, object.params); + // @ts-ignore this.emit(object.method!, object.params); }); } @@ -199,12 +195,10 @@ export class CRSession extends EventEmitter { } } -export class CDPSession extends EventEmitter { - static Events = { - Event: 'event', - Closed: 'close', - }; - +export class CDPSession extends ManagedEventEmitter<{ + event: [{ method: string, params: Object | undefined }] + closed: []; +}> { readonly guid: string; private _session: CRSession; private _listeners: RegisteredListener[] = []; @@ -212,8 +206,8 @@ export class CDPSession extends EventEmitter { constructor(parentSession: CRSession, sessionId: string) { super(); this.guid = `cdp-session@${sessionId}`; - this._session = parentSession.createChildSession(sessionId, (method, params) => this.emit(CDPSession.Events.Event, { method, params })); - this._listeners = [eventsHelper.addEventListener(parentSession, 'Target.detachedFromTarget', (event: Protocol.Target.detachedFromTargetPayload) => { + this._session = parentSession.createChildSession(sessionId, (method, params) => this.emit('event', { method, params })); + this._listeners = [parentSession.addManagedListener('Target.detachedFromTarget', (event: Protocol.Target.detachedFromTargetPayload) => { if (event.sessionId === sessionId) this._onClose(); })]; @@ -235,6 +229,6 @@ export class CDPSession extends EventEmitter { private _onClose() { eventsHelper.removeEventListeners(this._listeners); this._session.dispose(); - this.emit(CDPSession.Events.Closed); + this.emit('closed'); } } diff --git a/packages/playwright-core/src/server/chromium/crCoverage.ts b/packages/playwright-core/src/server/chromium/crCoverage.ts index ccb9b40ae0..44f5a28dc2 100644 --- a/packages/playwright-core/src/server/chromium/crCoverage.ts +++ b/packages/playwright-core/src/server/chromium/crCoverage.ts @@ -78,9 +78,9 @@ class JSCoverage { this._scriptIds.clear(); this._scriptSources.clear(); this._eventListeners = [ - eventsHelper.addEventListener(this._client, 'Debugger.scriptParsed', this._onScriptParsed.bind(this)), - eventsHelper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)), - eventsHelper.addEventListener(this._client, 'Debugger.paused', this._onDebuggerPaused.bind(this)), + this._client.addManagedListener('Debugger.scriptParsed', this._onScriptParsed.bind(this)), + this._client.addManagedListener('Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)), + this._client.addManagedListener('Debugger.paused', this._onDebuggerPaused.bind(this)), ]; await Promise.all([ this._client.send('Profiler.enable'), @@ -164,8 +164,8 @@ class CSSCoverage { this._stylesheetURLs.clear(); this._stylesheetSources.clear(); this._eventListeners = [ - eventsHelper.addEventListener(this._client, 'CSS.styleSheetAdded', this._onStyleSheet.bind(this)), - eventsHelper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)), + this._client.addManagedListener('CSS.styleSheetAdded', this._onStyleSheet.bind(this)), + this._client.addManagedListener('Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)), ]; await Promise.all([ this._client.send('DOM.enable'), diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts index 850f466afa..1724d60cfb 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts @@ -60,25 +60,25 @@ export class CRNetworkManager { async addSession(session: CRSession, workerFrame?: frames.Frame, isMain?: boolean) { const sessionInfo: SessionInfo = { session, isMain, workerFrame, eventListeners: [] }; sessionInfo.eventListeners = [ - eventsHelper.addEventListener(session, 'Fetch.requestPaused', this._onRequestPaused.bind(this, sessionInfo)), - eventsHelper.addEventListener(session, 'Fetch.authRequired', this._onAuthRequired.bind(this, sessionInfo)), - eventsHelper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, sessionInfo)), - eventsHelper.addEventListener(session, 'Network.requestWillBeSentExtraInfo', this._onRequestWillBeSentExtraInfo.bind(this)), - eventsHelper.addEventListener(session, 'Network.requestServedFromCache', this._onRequestServedFromCache.bind(this)), - eventsHelper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this, sessionInfo)), - eventsHelper.addEventListener(session, 'Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)), - eventsHelper.addEventListener(session, 'Network.loadingFinished', this._onLoadingFinished.bind(this, sessionInfo)), - eventsHelper.addEventListener(session, 'Network.loadingFailed', this._onLoadingFailed.bind(this, sessionInfo)), + session.addManagedListener('Fetch.requestPaused', this._onRequestPaused.bind(this, sessionInfo)), + session.addManagedListener('Fetch.authRequired', this._onAuthRequired.bind(this, sessionInfo)), + session.addManagedListener('Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, sessionInfo)), + session.addManagedListener('Network.requestWillBeSentExtraInfo', this._onRequestWillBeSentExtraInfo.bind(this)), + session.addManagedListener('Network.requestServedFromCache', this._onRequestServedFromCache.bind(this)), + session.addManagedListener('Network.responseReceived', this._onResponseReceived.bind(this, sessionInfo)), + session.addManagedListener('Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)), + session.addManagedListener('Network.loadingFinished', this._onLoadingFinished.bind(this, sessionInfo)), + session.addManagedListener('Network.loadingFailed', this._onLoadingFailed.bind(this, sessionInfo)), ]; if (this._page) { sessionInfo.eventListeners.push(...[ - eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page!._frameManager.onWebSocketCreated(e.requestId, e.url)), - eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!._frameManager.onWebSocketRequest(e.requestId)), - eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page!._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)), - eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)), - eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)), - eventsHelper.addEventListener(session, 'Network.webSocketClosed', e => this._page!._frameManager.webSocketClosed(e.requestId)), - eventsHelper.addEventListener(session, 'Network.webSocketFrameError', e => this._page!._frameManager.webSocketError(e.requestId, e.errorMessage)), + session.addManagedListener('Network.webSocketCreated', e => this._page!._frameManager.onWebSocketCreated(e.requestId, e.url)), + session.addManagedListener('Network.webSocketWillSendHandshakeRequest', e => this._page!._frameManager.onWebSocketRequest(e.requestId)), + session.addManagedListener('Network.webSocketHandshakeResponseReceived', e => this._page!._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)), + session.addManagedListener('Network.webSocketFrameSent', e => e.response.payloadData && this._page!._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)), + session.addManagedListener('Network.webSocketFrameReceived', e => e.response.payloadData && this._page!._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)), + session.addManagedListener('Network.webSocketClosed', e => this._page!._frameManager.webSocketClosed(e.requestId)), + session.addManagedListener('Network.webSocketFrameError', e => this._page!._frameManager.webSocketError(e.requestId, e.errorMessage)), ]); } this._sessions.set(session, sessionInfo); diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 53b96ad403..24cf84dd23 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -32,7 +32,7 @@ import type { Progress } from '../progress'; import type * as types from '../types'; import type * as channels from '@protocol/channels'; import { getAccessibilityTree } from './crAccessibility'; -import { CRBrowserContext } from './crBrowser'; +import type { CRBrowserContext } from './crBrowser'; import type { CRSession } from './crConnection'; import { CRCoverage } from './crCoverage'; import { DragManager } from './crDragDrop'; @@ -44,7 +44,6 @@ import { exceptionToError, releaseObject, toConsoleMessageLocation } from './crP import { platformToFontFamilies } from './defaultFontFamilies'; import type { Protocol } from './protocol'; import { VideoRecorder } from './videoRecorder'; -import { BrowserContext } from '../browserContext'; import { TargetClosedError } from '../errors'; import { isSessionClosedError } from '../protocolError'; @@ -131,7 +130,7 @@ export class CRPage implements PageDelegate { } private _reportAsNew(error?: Error) { - this._page.reportAsNew(error, this._isBackgroundPage ? CRBrowserContext.CREvents.BackgroundPage : BrowserContext.Events.Page); + this._page.reportAsNew(error, this._isBackgroundPage ? 'backgroundpage' : 'page'); } private async _forAllFrameSessions(cb: (frame: FrameSession) => Promise) { @@ -444,30 +443,30 @@ class FrameSession { private _addRendererListeners() { this._eventListeners.push(...[ - eventsHelper.addEventListener(this._client, 'Log.entryAdded', event => this._onLogEntryAdded(event)), - eventsHelper.addEventListener(this._client, 'Page.fileChooserOpened', event => this._onFileChooserOpened(event)), - eventsHelper.addEventListener(this._client, 'Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId)), - eventsHelper.addEventListener(this._client, 'Page.frameDetached', event => this._onFrameDetached(event.frameId, event.reason)), - eventsHelper.addEventListener(this._client, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)), - eventsHelper.addEventListener(this._client, 'Page.frameRequestedNavigation', event => this._onFrameRequestedNavigation(event)), - eventsHelper.addEventListener(this._client, 'Page.javascriptDialogOpening', event => this._onDialog(event)), - eventsHelper.addEventListener(this._client, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)), - eventsHelper.addEventListener(this._client, 'Runtime.bindingCalled', event => this._onBindingCalled(event)), - eventsHelper.addEventListener(this._client, 'Runtime.consoleAPICalled', event => this._onConsoleAPI(event)), - eventsHelper.addEventListener(this._client, 'Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails)), - eventsHelper.addEventListener(this._client, 'Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)), - eventsHelper.addEventListener(this._client, 'Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId)), - eventsHelper.addEventListener(this._client, 'Runtime.executionContextsCleared', event => this._onExecutionContextsCleared()), - eventsHelper.addEventListener(this._client, 'Target.attachedToTarget', event => this._onAttachedToTarget(event)), - eventsHelper.addEventListener(this._client, 'Target.detachedFromTarget', event => this._onDetachedFromTarget(event)), + this._client.addManagedListener('Log.entryAdded', event => this._onLogEntryAdded(event)), + this._client.addManagedListener('Page.fileChooserOpened', event => this._onFileChooserOpened(event)), + this._client.addManagedListener('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId)), + this._client.addManagedListener('Page.frameDetached', event => this._onFrameDetached(event.frameId, event.reason)), + this._client.addManagedListener('Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)), + this._client.addManagedListener('Page.frameRequestedNavigation', event => this._onFrameRequestedNavigation(event)), + this._client.addManagedListener('Page.javascriptDialogOpening', event => this._onDialog(event)), + this._client.addManagedListener('Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)), + this._client.addManagedListener('Runtime.bindingCalled', event => this._onBindingCalled(event)), + this._client.addManagedListener('Runtime.consoleAPICalled', event => this._onConsoleAPI(event)), + this._client.addManagedListener('Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails)), + this._client.addManagedListener('Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)), + this._client.addManagedListener('Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId)), + this._client.addManagedListener('Runtime.executionContextsCleared', event => this._onExecutionContextsCleared()), + this._client.addManagedListener('Target.attachedToTarget', event => this._onAttachedToTarget(event)), + this._client.addManagedListener('Target.detachedFromTarget', event => this._onDetachedFromTarget(event)), ]); } private _addBrowserListeners() { this._eventListeners.push(...[ - eventsHelper.addEventListener(this._client, 'Inspector.targetCrashed', event => this._onTargetCrashed()), - eventsHelper.addEventListener(this._client, 'Page.screencastFrame', event => this._onScreencastFrame(event)), - eventsHelper.addEventListener(this._client, 'Page.windowOpen', event => this._onWindowOpen(event)), + this._client.addManagedListener('Inspector.targetCrashed', event => this._onTargetCrashed()), + this._client.addManagedListener('Page.screencastFrame', event => this._onScreencastFrame(event)), + this._client.addManagedListener('Page.windowOpen', event => this._onWindowOpen(event)), ]); } @@ -531,11 +530,11 @@ class FrameSession { // hence we are going to get more lifecycle updates after the actual navigation has // started (even if the target url is about:blank). lifecycleEventsEnabled.catch(e => {}).then(() => { - this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event))); + this._eventListeners.push(this._client.addManagedListener('Page.lifecycleEvent', event => this._onLifecycleEvent(event))); }); } else { this._firstNonInitialNavigationCommittedFulfill(); - this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event))); + this._eventListeners.push(this._client.addManagedListener('Page.lifecycleEvent', event => this._onLifecycleEvent(event))); } }), this._client.send('Log.enable', {}), @@ -770,7 +769,7 @@ class FrameSession { const args = event.args.map(o => worker._existingExecutionContext!.createHandle(o)); this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace)); }); - session.on('Runtime.exceptionThrown', exception => this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, exceptionToError(exception.exceptionDetails), this._page)); + session.on('Runtime.exceptionThrown', exception => this._page.emitOnContextOnceInitialized('pageerror', exceptionToError(exception.exceptionDetails), this._page)); } _onDetachedFromTarget(event: Protocol.Target.detachedFromTargetPayload) { @@ -865,7 +864,7 @@ class FrameSession { _onDialog(event: Protocol.Page.javascriptDialogOpeningPayload) { if (!this._page._frameManager.frame(this._targetId)) return; // Our frame/subtree may be gone already. - this._page.emitOnContext(BrowserContext.Events.Dialog, new dialog.Dialog( + this._page.emitOnContext('dialog', new dialog.Dialog( this._page, event.type, event.message, @@ -876,7 +875,7 @@ class FrameSession { } _handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) { - this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, exceptionToError(exceptionDetails), this._page); + this._page.emitOnContextOnceInitialized('pageerror', exceptionToError(exceptionDetails), this._page); } async _onTargetCrashed() { @@ -929,7 +928,7 @@ class FrameSession { this._client.send('Page.screencastFrameAck', { sessionId: payload.sessionId }).catch(() => {}); }); const buffer = Buffer.from(payload.data, 'base64'); - this._page.emit(Page.Events.ScreencastFrame, { + this._page.emit('screencastframe', { buffer, timestamp: payload.metadata.timestamp, width: payload.metadata.deviceWidth, @@ -947,7 +946,7 @@ class FrameSession { async _startVideoRecording(options: types.PageScreencastOptions) { const screencastId = this._screencastId; assert(screencastId); - this._page.once(Page.Events.Close, () => this._stopVideoRecording().catch(() => {})); + this._page.once('close', () => this._stopVideoRecording().catch(() => {})); const gotFirstFrame = new Promise(f => this._client.once('Page.screencastFrame', f)); await this._startScreencast(this._videoRecorder, { format: 'jpeg', diff --git a/packages/playwright-core/src/server/chromium/crServiceWorker.ts b/packages/playwright-core/src/server/chromium/crServiceWorker.ts index 730504866b..b403c7e55f 100644 --- a/packages/playwright-core/src/server/chromium/crServiceWorker.ts +++ b/packages/playwright-core/src/server/chromium/crServiceWorker.ts @@ -19,7 +19,6 @@ import type { CRSession } from './crConnection'; import { CRExecutionContext } from './crExecutionContext'; import { CRNetworkManager } from './crNetworkManager'; import * as network from '../network'; -import { BrowserContext } from '../browserContext'; export class CRServiceWorker extends Worker { readonly _browserContext: CRBrowserContext; @@ -87,19 +86,19 @@ export class CRServiceWorker extends Worker { } reportRequestFinished(request: network.Request, response: network.Response | null) { - this._browserContext.emit(BrowserContext.Events.RequestFinished, { request, response }); + this._browserContext.emit('requestfinished', { request, response }); } requestFailed(request: network.Request, _canceled: boolean) { - this._browserContext.emit(BrowserContext.Events.RequestFailed, request); + this._browserContext.emit('requestfailed', request); } requestReceivedResponse(response: network.Response) { - this._browserContext.emit(BrowserContext.Events.Response, response); + this._browserContext.emit('response', response); } requestStarted(request: network.Request, route?: network.RouteDelegate) { - this._browserContext.emit(BrowserContext.Events.Request, request); + this._browserContext.emit('request', request); if (route) { const r = new network.Route(request, route); if (this._browserContext._requestInterceptor?.(r, request)) diff --git a/packages/playwright-core/src/server/chromium/videoRecorder.ts b/packages/playwright-core/src/server/chromium/videoRecorder.ts index bfb377e79b..e706dbb2f5 100644 --- a/packages/playwright-core/src/server/chromium/videoRecorder.ts +++ b/packages/playwright-core/src/server/chromium/videoRecorder.ts @@ -16,7 +16,7 @@ import type { ChildProcess } from 'child_process'; import { assert, monotonicTime } from '../../utils'; -import { Page } from '../page'; +import type { Page } from '../page'; import { launchProcess } from '../../utils/processLauncher'; import type { Progress } from '../progress'; import { ProgressController } from '../progress'; @@ -53,7 +53,7 @@ export class VideoRecorder { private constructor(page: Page, ffmpegPath: string, progress: Progress) { this._progress = progress; this._ffmpegPath = ffmpegPath; - page.on(Page.Events.ScreencastFrame, frame => this.writeFrame(frame.buffer, frame.timestamp)); + page.on('screencastframe', frame => this.writeFrame(frame.buffer, frame.timestamp!)); } private async _launch(options: types.PageScreencastOptions) { diff --git a/packages/playwright-core/src/server/debugController.ts b/packages/playwright-core/src/server/debugController.ts index ea2dc9a805..4e3348e220 100644 --- a/packages/playwright-core/src/server/debugController.ts +++ b/packages/playwright-core/src/server/debugController.ts @@ -28,15 +28,13 @@ import type { Language } from '../utils/isomorphic/locatorGenerators'; const internalMetadata = serverSideCallMetadata(); -export class DebugController extends SdkObject { - static Events = { - StateChanged: 'stateChanged', - InspectRequested: 'inspectRequested', - SourceChanged: 'sourceChanged', - Paused: 'paused', - SetModeRequested: 'setModeRequested', - }; - +export class DebugController extends SdkObject<{ + stateChanged: [{ pageCount: number }] + inspectRequested: [{ selector: string, locator: string }] + sourceChanged: [{ text: string, header: string | undefined, footer: string | undefined, actions: string[] | undefined }] + paused: [{ paused: boolean }] + setModeRequested: [{ mode: Mode }] +}> { private _autoCloseTimer: NodeJS.Timeout | undefined; // TODO: remove in 1.27 private _autoCloseAllowed = false; @@ -193,7 +191,7 @@ export class DebugController extends SdkObject { pageCount += context.pages().length; } } - this.emit(DebugController.Events.StateChanged, { pageCount }); + this.emit('stateChanged', { pageCount }); } private async _allRecorders(): Promise { @@ -226,20 +224,20 @@ class InspectingRecorderApp extends EmptyRecorderApp { override async setSelector(selector: string): Promise { const locator: string = asLocator(this._debugController._sdkLanguage, selector); - this._debugController.emit(DebugController.Events.InspectRequested, { selector, locator }); + this._debugController.emit('inspectRequested', { selector, locator }); } override async setSources(sources: Source[]): Promise { const source = sources.find(s => s.id === this._debugController._codegenId); const { text, header, footer, actions } = source || { text: '' }; - this._debugController.emit(DebugController.Events.SourceChanged, { text, header, footer, actions }); + this._debugController.emit('sourceChanged', { text, header, footer, actions }); } override async setPaused(paused: boolean) { - this._debugController.emit(DebugController.Events.Paused, { paused }); + this._debugController.emit('paused', { paused }); } override async setMode(mode: Mode) { - this._debugController.emit(DebugController.Events.SetModeRequested, { mode }); + this._debugController.emit('setModeRequested', { mode }); } } diff --git a/packages/playwright-core/src/server/debugger.ts b/packages/playwright-core/src/server/debugger.ts index 420b438ccb..0adbbf8760 100644 --- a/packages/playwright-core/src/server/debugger.ts +++ b/packages/playwright-core/src/server/debugger.ts @@ -16,21 +16,20 @@ import { EventEmitter } from 'events'; import { debugMode, isUnderTest, monotonicTime } from '../utils'; -import { BrowserContext } from './browserContext'; +import type { BrowserContext } from './browserContext'; import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation'; import { commandsWithTracingSnapshots, pausesBeforeInputActions, slowMoActions } from '../protocol/debug'; const symbol = Symbol('Debugger'); -export class Debugger extends EventEmitter implements InstrumentationListener { +export class Debugger extends EventEmitter<{ + pausedstatechanged: [] +}> implements InstrumentationListener { private _pauseOnNextStatement = false; private _pausedCallsMetadata = new Map void, sdkObject: SdkObject }>(); private _enabled: boolean; private _context: BrowserContext; - static Events = { - PausedStateChanged: 'pausedstatechanged' - }; private _muted = false; private _slowMo: number | undefined; @@ -42,7 +41,7 @@ export class Debugger extends EventEmitter implements InstrumentationListener { if (this._enabled) this.pauseOnNextStatement(); context.instrumentation.addListener(this, context); - this._context.once(BrowserContext.Events.Close, () => { + this._context.once('close', () => { this._context.instrumentation.removeListener(this); }); this._slowMo = this._context._browser.options.slowMo; @@ -83,7 +82,7 @@ export class Debugger extends EventEmitter implements InstrumentationListener { const result = new Promise(resolve => { this._pausedCallsMetadata.set(metadata, { resolve, sdkObject }); }); - this.emit(Debugger.Events.PausedStateChanged); + this.emit('pausedstatechanged'); return result; } @@ -98,7 +97,7 @@ export class Debugger extends EventEmitter implements InstrumentationListener { resolve(); } this._pausedCallsMetadata.clear(); - this.emit(Debugger.Events.PausedStateChanged); + this.emit('pausedstatechanged'); } pauseOnNextStatement() { diff --git a/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts b/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts index 7198229f53..97fe0f29ed 100644 --- a/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts @@ -17,7 +17,7 @@ import type { RootDispatcher } from './dispatcher'; import { Dispatcher, existingDispatcher } from './dispatcher'; import type { Android, SocketBackend } from '../android/android'; -import { AndroidDevice } from '../android/android'; +import type { AndroidDevice } from '../android/android'; import type * as channels from '@protocol/channels'; import { BrowserContextDispatcher } from './browserContextDispatcher'; import type { CallMetadata } from '../instrumentation'; @@ -56,9 +56,9 @@ export class AndroidDeviceDispatcher extends Dispatcher this._dispatchEvent('webViewAdded', { webView })); - this.addObjectListener(AndroidDevice.Events.WebViewRemoved, socketName => this._dispatchEvent('webViewRemoved', { socketName })); - this.addObjectListener(AndroidDevice.Events.Close, socketName => this._dispatchEvent('close')); + this.addObjectListener('webViewAdded', webView => this._dispatchEvent('webViewAdded', { webView })); + this.addObjectListener('webViewRemoved', socketName => this._dispatchEvent('webViewRemoved', { socketName })); + this.addObjectListener('close', () => this._dispatchEvent('close')); } async wait(params: channels.AndroidDeviceWaitParams) { diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 5654950360..3368ce7365 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { BrowserContext } from '../browserContext'; +import type { BrowserContext } from '../browserContext'; import { Dispatcher, existingDispatcher } from './dispatcher'; import type { DispatcherScope } from './dispatcher'; import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher'; import type { FrameDispatcher } from './frameDispatcher'; import type * as channels from '@protocol/channels'; import { RouteDispatcher, RequestDispatcher, ResponseDispatcher, APIRequestContextDispatcher } from './networkDispatchers'; -import { CRBrowserContext } from '../chromium/crBrowser'; +import type { CRBrowserContext } from '../chromium/crBrowser'; import { CDPSessionDispatcher } from './cdpSessionDispatcher'; import { Recorder } from '../recorder'; import type { CallMetadata } from '../instrumentation'; @@ -71,7 +71,7 @@ export class BrowserContextDispatcher extends Dispatcher { + this.addObjectListener('page', page => { this._dispatchEvent('page', { page: PageDispatcher.from(this, page) }); }); - this.addObjectListener(BrowserContext.Events.Close, () => { + this.addObjectListener('close', () => { this._dispatchEvent('close'); this._dispose(); }); - this.addObjectListener(BrowserContext.Events.PageError, (error: Error, page: Page) => { + this.addObjectListener('pageerror', (error: Error, page: Page) => { this._dispatchEvent('pageError', { error: serializeError(error), page: PageDispatcher.from(this, page) }); }); - this.addObjectListener(BrowserContext.Events.Console, (message: ConsoleMessage) => { + this.addObjectListener('console', (message: ConsoleMessage) => { const page = message.page()!; if (this._shouldDispatchEvent(page, 'console')) { const pageDispatcher = PageDispatcher.from(this, page); @@ -102,7 +102,7 @@ export class BrowserContextDispatcher extends Dispatcher { + this.addObjectListener('dialog', (dialog: Dialog) => { if (this._shouldDispatchEvent(dialog.page(), 'dialog')) this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this, dialog) }); else @@ -112,12 +112,12 @@ export class BrowserContextDispatcher extends Dispatcher this._dispatchEvent('backgroundPage', { page: PageDispatcher.from(this, page) })); + this.addObjectListener('backgroundpage', page => this._dispatchEvent('backgroundPage', { page: PageDispatcher.from(this, page) })); for (const serviceWorker of (context as CRBrowserContext).serviceWorkers()) this._dispatchEvent('serviceWorker', { worker: new WorkerDispatcher(this, serviceWorker) }); - this.addObjectListener(CRBrowserContext.CREvents.ServiceWorker, serviceWorker => this._dispatchEvent('serviceWorker', { worker: new WorkerDispatcher(this, serviceWorker) })); + this.addObjectListener('serviceworker', serviceWorker => this._dispatchEvent('serviceWorker', { worker: new WorkerDispatcher(this, serviceWorker) })); } - this.addObjectListener(BrowserContext.Events.Request, (request: Request) => { + this.addObjectListener('request', (request: Request) => { // Create dispatcher, if: // - There are listeners to the requests. // - We are redirected from a reported request so that redirectedTo was updated on client. @@ -132,7 +132,7 @@ export class BrowserContextDispatcher extends Dispatcher { + this.addObjectListener('response', (response: Response) => { const requestDispatcher = existingDispatcher(response.request()); if (!requestDispatcher && !this._shouldDispatchNetworkEvent(response.request(), 'response')) return; @@ -141,7 +141,7 @@ export class BrowserContextDispatcher extends Dispatcher { + this.addObjectListener('requestfailed', (request: Request) => { const requestDispatcher = existingDispatcher(request); if (!requestDispatcher && !this._shouldDispatchNetworkEvent(request, 'requestFailed')) return; @@ -152,7 +152,7 @@ export class BrowserContextDispatcher extends Dispatcher { + this.addObjectListener('requestfinished', ({ request, response }: { request: Request, response: Response | null }) => { const requestDispatcher = existingDispatcher(request); if (!requestDispatcher && !this._shouldDispatchNetworkEvent(request, 'requestFinished')) return; diff --git a/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts index 1f23518bcb..bf23584e70 100644 --- a/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Browser } from '../browser'; +import type { Browser } from '../browser'; import type * as channels from '@protocol/channels'; import { BrowserContextDispatcher } from './browserContextDispatcher'; import { CDPSessionDispatcher } from './cdpSessionDispatcher'; @@ -24,7 +24,7 @@ import { Dispatcher } from './dispatcher'; import type { CRBrowser } from '../chromium/crBrowser'; import type { PageDispatcher } from './pageDispatcher'; import type { CallMetadata } from '../instrumentation'; -import { BrowserContext } from '../browserContext'; +import type { BrowserContext } from '../browserContext'; import { Selectors } from '../selectors'; import type { BrowserTypeDispatcher } from './browserTypeDispatcher'; import { ArtifactDispatcher } from './artifactDispatcher'; @@ -35,8 +35,8 @@ export class BrowserDispatcher extends Dispatcher this._dispatchEvent('beforeClose')); - this.addObjectListener(Browser.Events.Disconnected, () => this._didClose()); + this.addObjectListener('beforeClose', () => this._dispatchEvent('beforeClose')); + this.addObjectListener('disconnected', () => this._didClose()); } override _onDispose() { @@ -109,7 +109,7 @@ export class ConnectedBrowserDispatcher extends Dispatcher this.emit('beforeClose')); + this.addObjectListener('beforeClose', () => this.emit('beforeClose')); // When we have a remotely-connected browser, each client gets a fresh Selector instance, // so that two clients do not interfere between each other. this.selectors = new Selectors(); @@ -121,7 +121,7 @@ export class ConnectedBrowserDispatcher extends Dispatcher this._contexts.delete(context)); + context.on('close', () => this._contexts.delete(context)); return { context: new BrowserContextDispatcher(this, context) }; } diff --git a/packages/playwright-core/src/server/dispatchers/cdpSessionDispatcher.ts b/packages/playwright-core/src/server/dispatchers/cdpSessionDispatcher.ts index 33fa19f5e7..e7e49d677c 100644 --- a/packages/playwright-core/src/server/dispatchers/cdpSessionDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/cdpSessionDispatcher.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { CDPSession } from '../chromium/crConnection'; +import type { CDPSession } from '../chromium/crConnection'; import type * as channels from '@protocol/channels'; import { Dispatcher } from './dispatcher'; import type { BrowserDispatcher } from './browserDispatcher'; @@ -26,8 +26,8 @@ export class CDPSessionDispatcher extends Dispatcher this._dispatchEvent('event', { method, params })); - this.addObjectListener(CDPSession.Events.Closed, () => this._dispose()); + this.addObjectListener('event', ({ method, params }) => this._dispatchEvent('event', { method, params })); + this.addObjectListener('closed', () => this._dispose()); } async send(params: channels.CDPSessionSendParams): Promise { diff --git a/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts b/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts index 34c4d3b4ca..f63fe778c5 100644 --- a/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts @@ -17,7 +17,7 @@ import type * as channels from '@protocol/channels'; import { eventsHelper } from '../../utils'; import type { RegisteredListener } from '../../utils/eventsHelper'; -import { DebugController } from '../debugController'; +import type { DebugController } from '../debugController'; import type { DispatcherConnection, RootDispatcher } from './dispatcher'; import { Dispatcher } from './dispatcher'; @@ -29,19 +29,19 @@ export class DebugControllerDispatcher extends Dispatcher { + this._object.addManagedListener('stateChanged', params => { this._dispatchEvent('stateChanged', params); }), - eventsHelper.addEventListener(this._object, DebugController.Events.InspectRequested, ({ selector, locator }) => { + this._object.addManagedListener('inspectRequested', ({ selector, locator }) => { this._dispatchEvent('inspectRequested', { selector, locator }); }), - eventsHelper.addEventListener(this._object, DebugController.Events.SourceChanged, ({ text, header, footer, actions }) => { + this._object.addManagedListener('sourceChanged', ({ text, header, footer, actions }) => { this._dispatchEvent('sourceChanged', ({ text, header, footer, actions })); }), - eventsHelper.addEventListener(this._object, DebugController.Events.Paused, ({ paused }) => { + this._object.addManagedListener('paused', ({ paused }) => { this._dispatchEvent('paused', ({ paused })); }), - eventsHelper.addEventListener(this._object, DebugController.Events.SetModeRequested, ({ mode }) => { + this._object.addManagedListener('setModeRequested', ({ mode }) => { this._dispatchEvent('setModeRequested', ({ mode })); }), ]; diff --git a/packages/playwright-core/src/server/dispatchers/dispatcher.ts b/packages/playwright-core/src/server/dispatchers/dispatcher.ts index e14960b495..1306810aa1 100644 --- a/packages/playwright-core/src/server/dispatchers/dispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/dispatcher.ts @@ -23,7 +23,7 @@ import type { CallMetadata } from '../instrumentation'; import { SdkObject } from '../instrumentation'; import type { PlaywrightDispatcher } from './playwrightDispatcher'; import { eventsHelper } from '../..//utils/eventsHelper'; -import type { RegisteredListener } from '../..//utils/eventsHelper'; +import type { ManagedEventEmitter, RegisteredListener } from '../..//utils/eventsHelper'; import { isProtocolError } from '../protocolError'; export const dispatcherSymbol = Symbol('dispatcher'); @@ -44,7 +44,7 @@ function maxDispatchersForBucket(gcBucket: string) { }[gcBucket] ?? 10000; } -export class Dispatcher extends EventEmitter implements channels.Channel { +export class Dispatcher & { guid: string }) | { guid: string; _events?: void }, ChannelType, ParentScopeType extends DispatcherScope> extends EventEmitter implements channels.Channel { private _connection: DispatcherConnection; // Parent is always "isScope". private _parent: ParentScopeType | undefined; @@ -88,8 +88,12 @@ export class Dispatcher void) { - this._eventListeners.push(eventsHelper.addEventListener(this._object as unknown as EventEmitter, eventName, handler)); + addObjectListener( + eventName: K, + handler: (...args: Type['_events'][K]) => void + ) { + // @ts-ignore + this._eventListeners.push(eventsHelper.addEventListener(this._object, eventName, handler)); } adopt(child: DispatcherScope) { diff --git a/packages/playwright-core/src/server/dispatchers/electronDispatcher.ts b/packages/playwright-core/src/server/dispatchers/electronDispatcher.ts index a4e28da26f..0e3bc40c48 100644 --- a/packages/playwright-core/src/server/dispatchers/electronDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/electronDispatcher.ts @@ -17,7 +17,7 @@ import type { RootDispatcher } from './dispatcher'; import { Dispatcher } from './dispatcher'; import type { Electron } from '../electron/electron'; -import { ElectronApplication } from '../electron/electron'; +import type { ElectronApplication } from '../electron/electron'; import type * as channels from '@protocol/channels'; import { BrowserContextDispatcher } from './browserContextDispatcher'; import type { PageDispatcher } from './pageDispatcher'; @@ -47,11 +47,11 @@ export class ElectronApplicationDispatcher extends Dispatcher { + this.addObjectListener('close', () => { this._dispatchEvent('close'); this._dispose(); }); - this.addObjectListener(ElectronApplication.Events.Console, (message: ConsoleMessage) => { + this.addObjectListener('console', (message: ConsoleMessage) => { if (!this._subscriptions.has('console')) return; this._dispatchEvent('console', { diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index 6dcb9a7220..a8554adb2e 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -15,7 +15,7 @@ */ import type { NavigationEvent } from '../frames'; -import { Frame } from '../frames'; +import type { Frame } from '../frames'; import type * as channels from '@protocol/channels'; import { Dispatcher, existingDispatcher } from './dispatcher'; import { ElementHandleDispatcher } from './elementHandlerDispatcher'; @@ -58,13 +58,13 @@ export class FrameDispatcher extends Dispatcher { + this.addObjectListener('addlifecycle', lifecycleEvent => { this._dispatchEvent('loadstate', { add: lifecycleEvent }); }); - this.addObjectListener(Frame.Events.RemoveLifecycle, lifecycleEvent => { + this.addObjectListener('removelifecycle', lifecycleEvent => { this._dispatchEvent('loadstate', { remove: lifecycleEvent }); }); - this.addObjectListener(Frame.Events.InternalNavigation, (event: NavigationEvent) => { + this.addObjectListener('internalnavigation', (event: NavigationEvent) => { if (!event.isPublic) return; const params = { url: event.url, name: event.name, error: event.error ? event.error.message : undefined }; diff --git a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts index ba600f697e..f0aee326e0 100644 --- a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts +++ b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts @@ -18,7 +18,7 @@ import type * as channels from '@protocol/channels'; import type { APIRequestContext } from '../fetch'; import type { CallMetadata } from '../instrumentation'; import type { Request, Response, Route } from '../network'; -import { WebSocket } from '../network'; +import type { WebSocket } from '../network'; import type { RootDispatcher } from './dispatcher'; import { Dispatcher, existingDispatcher } from './dispatcher'; import { TracingDispatcher } from './tracingDispatcher'; @@ -164,10 +164,10 @@ export class WebSocketDispatcher extends Dispatcher this._dispatchEvent('frameSent', event)); - this.addObjectListener(WebSocket.Events.FrameReceived, (event: { opcode: number, data: string }) => this._dispatchEvent('frameReceived', event)); - this.addObjectListener(WebSocket.Events.SocketError, (error: string) => this._dispatchEvent('socketError', { error })); - this.addObjectListener(WebSocket.Events.Close, () => this._dispatchEvent('close', {})); + this.addObjectListener('framesent', (event: { opcode: number, data: string }) => this._dispatchEvent('frameSent', event)); + this.addObjectListener('framereceived', (event: { opcode: number, data: string }) => this._dispatchEvent('frameReceived', event)); + this.addObjectListener('socketerror', (error: string) => this._dispatchEvent('socketError', { error })); + this.addObjectListener('close', () => this._dispatchEvent('close', {})); } } diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 9acc7378b2..d2b84ab03f 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -16,7 +16,8 @@ import type { BrowserContext } from '../browserContext'; import type { Frame } from '../frames'; -import { Page, Worker } from '../page'; +import type { Worker } from '../page'; +import type { Page } from '../page'; import type * as channels from '@protocol/channels'; import { Dispatcher, existingDispatcher } from './dispatcher'; import { parseError } from '../errors'; @@ -70,25 +71,25 @@ export class PageDispatcher extends Dispatcher { + this.addObjectListener('close', () => { this._dispatchEvent('close'); this._dispose(); }); - this.addObjectListener(Page.Events.Crash, () => this._dispatchEvent('crash')); - this.addObjectListener(Page.Events.Download, (download: Download) => { + this.addObjectListener('crash', () => this._dispatchEvent('crash')); + this.addObjectListener('download', (download: Download) => { // Artifact can outlive the page, so bind to the context scope. 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('filechooser', (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', { element: ElementHandleDispatcher.from(mainFrame, fileChooser.element()), isMultiple: fileChooser.isMultiple() })); - this.addObjectListener(Page.Events.FrameAttached, frame => this._onFrameAttached(frame)); - this.addObjectListener(Page.Events.FrameDetached, frame => this._onFrameDetached(frame)); - this.addObjectListener(Page.Events.LocatorHandlerTriggered, (uid: number) => this._dispatchEvent('locatorHandlerTriggered', { uid })); - 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: ArtifactDispatcher.from(parentScope, artifact) })); + this.addObjectListener('frameattached', frame => this._onFrameAttached(frame)); + this.addObjectListener('framedetached', frame => this._onFrameDetached(frame)); + this.addObjectListener('locatorhandlertriggered', (uid: number) => this._dispatchEvent('locatorHandlerTriggered', { uid })); + this.addObjectListener('websocket', webSocket => this._dispatchEvent('webSocket', { webSocket: new WebSocketDispatcher(this, webSocket) })); + this.addObjectListener('worker', worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this, worker) })); + this.addObjectListener('video', (artifact: Artifact) => this._dispatchEvent('video', { artifact: ArtifactDispatcher.from(parentScope, artifact) })); if (page._video) this._dispatchEvent('video', { artifact: ArtifactDispatcher.from(this.parentScope(), page._video) }); // Ensure client knows about all frames. @@ -338,7 +339,7 @@ export class WorkerDispatcher extends Dispatcher this._dispatchEvent('close')); + this.addObjectListener('close', () => this._dispatchEvent('close')); } async evaluateExpression(params: channels.WorkerEvaluateExpressionParams, metadata: CallMetadata): Promise { diff --git a/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts b/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts index 2122f064c9..c61d7d75b0 100644 --- a/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts @@ -19,7 +19,7 @@ import type { Browser } from '../browser'; import { GlobalAPIRequestContext } from '../fetch'; import type { Playwright } from '../playwright'; import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../../common/socksProxy'; -import { SocksProxy } from '../../common/socksProxy'; +import type { SocksProxy } from '../../common/socksProxy'; import { AndroidDispatcher } from './androidDispatcher'; import { BrowserTypeDispatcher } from './browserTypeDispatcher'; import type { RootDispatcher } from './dispatcher'; @@ -79,9 +79,9 @@ class SocksSupportDispatcher extends Dispatcher<{ guid: string }, channels.Socks this._type_SocksSupport = true; this._socksProxy = socksProxy; this._socksListeners = [ - eventsHelper.addEventListener(socksProxy, SocksProxy.Events.SocksRequested, (payload: SocksSocketRequestedPayload) => this._dispatchEvent('socksRequested', payload)), - eventsHelper.addEventListener(socksProxy, SocksProxy.Events.SocksData, (payload: SocksSocketDataPayload) => this._dispatchEvent('socksData', payload)), - eventsHelper.addEventListener(socksProxy, SocksProxy.Events.SocksClosed, (payload: SocksSocketClosedPayload) => this._dispatchEvent('socksClosed', payload)), + socksProxy.addManagedListener('socksRequested', (payload: SocksSocketRequestedPayload) => this._dispatchEvent('socksRequested', payload)), + socksProxy.addManagedListener('socksData', (payload: SocksSocketDataPayload) => this._dispatchEvent('socksData', payload)), + socksProxy.addManagedListener('socksClosed', (payload: SocksSocketClosedPayload) => this._dispatchEvent('socksClosed', payload)), ]; } diff --git a/packages/playwright-core/src/server/download.ts b/packages/playwright-core/src/server/download.ts index f7a92c8c7d..667935ca57 100644 --- a/packages/playwright-core/src/server/download.ts +++ b/packages/playwright-core/src/server/download.ts @@ -15,7 +15,7 @@ */ import path from 'path'; -import { Page } from './page'; +import type { Page } from './page'; import { assert } from '../utils'; import { Artifact } from './artifact'; @@ -35,13 +35,13 @@ export class Download { this._suggestedFilename = suggestedFilename; page._browserContext._downloads.add(this); if (suggestedFilename !== undefined) - this._page.emit(Page.Events.Download, this); + this._page.emit('download', this); } _filenameSuggested(suggestedFilename: string) { assert(this._suggestedFilename === undefined); this._suggestedFilename = suggestedFilename; - this._page.emit(Page.Events.Download, this); + this._page.emit('download', this); } suggestedFilename(): string { diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index b8f361b48a..319f13298b 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -49,12 +49,10 @@ import { ConsoleMessage } from '../console'; const ARTIFACTS_FOLDER = path.join(os.tmpdir(), 'playwright-artifacts-'); -export class ElectronApplication extends SdkObject { - static Events = { - Close: 'close', - Console: 'console', - }; - +export class ElectronApplication extends SdkObject<{ + close: [], + console: [ConsoleMessage] +}> { private _browserContext: CRBrowserContext; private _nodeConnection: CRConnection; private _nodeSession: CRSession; @@ -83,7 +81,7 @@ export class ElectronApplication extends SdkObject { this._nodeElectronHandlePromise.resolve(new js.JSHandle(this._nodeExecutionContext!, 'object', 'ElectronModule', remoteObject.objectId!)); }); this._nodeSession.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event)); - const appClosePromise = new Promise(f => this.once(ElectronApplication.Events.Close, f)); + const appClosePromise = new Promise(f => this.once('close', f)); this._browserContext.setCustomCloseHandler(async () => { await this._browserContext.stopVideoRecording(); const electronHandle = await this._nodeElectronHandlePromise; @@ -114,7 +112,7 @@ export class ElectronApplication extends SdkObject { return; const args = event.args.map(arg => this._nodeExecutionContext!.createHandle(arg)); const message = new ConsoleMessage(null, event.type, undefined, args, toConsoleMessageLocation(event.stackTrace)); - this.emit(ElectronApplication.Events.Console, message); + this.emit('console', message); } async initialize() { @@ -226,7 +224,7 @@ export class Electron extends SdkObject { handleSIGINT: true, handleSIGTERM: true, handleSIGHUP: true, - onExit: () => app?.emit(ElectronApplication.Events.Close), + onExit: () => app?.emit('close'), }); // All waitForLines must be started immediately. diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index f5dad1d656..bb2150e614 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -28,7 +28,8 @@ import { TimeoutSettings } from '../common/timeoutSettings'; import { getUserAgent } from '../utils/userAgent'; import { assert, createGuid, monotonicTime } from '../utils'; import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle'; -import { BrowserContext, verifyClientCertificates } from './browserContext'; +import type { BrowserContext } from './browserContext'; +import { verifyClientCertificates } from './browserContext'; import { CookieStore, domainMatches } from './cookieStore'; import { MultipartFormData } from './formData'; import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs'; @@ -82,14 +83,11 @@ type SendRequestOptions = https.RequestOptions & { __testHookLookup?: (hostname: string) => LookupAddress[] }; -export abstract class APIRequestContext extends SdkObject { - static Events = { - Dispose: 'dispose', - - Request: 'request', - RequestFinished: 'requestfinished', - }; - +export abstract class APIRequestContext extends SdkObject<{ + dispose: [] + request: [APIRequestEvent] + requestfinished: [APIRequestFinishedEvent] +}> { readonly fetchResponses: Map = new Map(); readonly fetchLog: Map = new Map(); protected static allInstances: Set = new Set(); @@ -114,7 +112,7 @@ export abstract class APIRequestContext extends SdkObject { APIRequestContext.allInstances.delete(this); this.fetchResponses.clear(); this.fetchLog.clear(); - this.emit(APIRequestContext.Events.Dispose); + this.emit('dispose'); } disposeResponse(fetchUid: string) { @@ -289,7 +287,7 @@ export abstract class APIRequestContext extends SdkObject { cookies: requestCookies, postData }; - this.emit(APIRequestContext.Events.Request, requestEvent); + this.emit('request', requestEvent); return new Promise((fulfill, reject) => { const requestConstructor: ((url: URL, options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void) => http.ClientRequest) @@ -309,7 +307,7 @@ export abstract class APIRequestContext extends SdkObject { cookies, body }; - this.emit(APIRequestContext.Events.RequestFinished, requestFinishedEvent); + this.emit('requestfinished', requestFinishedEvent); }; progress.log(`← ${response.statusCode} ${response.statusMessage}`); for (const [name, value] of Object.entries(response.headers)) @@ -447,8 +445,8 @@ export abstract class APIRequestContext extends SdkObject { reject(new Error('Request context disposed.')); request.destroy(); }; - this.on(APIRequestContext.Events.Dispose, disposeListener); - request.on('close', () => this.off(APIRequestContext.Events.Dispose, disposeListener)); + this.on('dispose', disposeListener); + request.on('close', () => this.off('dispose', disposeListener)); progress.log(`→ ${options.method} ${url.toString()}`); if (options.headers) { @@ -508,7 +506,7 @@ export class BrowserContextAPIRequestContext extends APIRequestContext { constructor(context: BrowserContext) { super(context); this._context = context; - context.once(BrowserContext.Events.Close, () => this._disposeImpl()); + context.once('close', () => this._disposeImpl()); } override tracing() { diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index 7ed2d8cb2f..89dd334a01 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -25,7 +25,7 @@ import type { InitScript, Page, PageBinding, PageDelegate } from '../page'; import type { ConnectionTransport } from '../transport'; import type * as types from '../types'; import type * as channels from '@protocol/channels'; -import { ConnectionEvents, FFConnection, type FFSession } from './ffConnection'; +import { FFConnection, type FFSession } from './ffConnection'; import { FFPage } from './ffPage'; import type { Protocol } from './protocol'; import type { SdkObject } from '../instrumentation'; @@ -69,7 +69,7 @@ export class FFBrowser extends Browser { this.session = connection.rootSession; this._ffPages = new Map(); this._contexts = new Map(); - this._connection.on(ConnectionEvents.Disconnected, () => this._onDisconnect()); + this._connection.on('disconnected', () => this._onDisconnect()); this.session.on('Browser.attachedToTarget', this._onAttachedToTarget.bind(this)); this.session.on('Browser.detachedFromTarget', this._onDetachedFromTarget.bind(this)); this.session.on('Browser.downloadCreated', this._onDownloadCreated.bind(this)); diff --git a/packages/playwright-core/src/server/firefox/ffConnection.ts b/packages/playwright-core/src/server/firefox/ffConnection.ts index 1a24e1dbf0..1c2e3ff012 100644 --- a/packages/playwright-core/src/server/firefox/ffConnection.ts +++ b/packages/playwright-core/src/server/firefox/ffConnection.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import { EventEmitter } from 'events'; import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import type { Protocol } from './protocol'; import type { RecentLogsCollector } from '../../utils/debugLogger'; @@ -23,16 +22,15 @@ import { debugLogger } from '../../utils/debugLogger'; import type { ProtocolLogger } from '../types'; import { helper } from '../helper'; import { ProtocolError } from '../protocolError'; - -export const ConnectionEvents = { - Disconnected: Symbol('Disconnected'), -}; +import { ManagedEventEmitter } from '../../utils'; // FFPlaywright uses this special id to issue Browser.close command which we // should ignore. export const kBrowserCloseMessageId = -9999; -export class FFConnection extends EventEmitter { +export class FFConnection extends ManagedEventEmitter<{ [K in keyof Protocol.Events]: [Protocol.Events[K]] } & { + 'disconnected': [], +}> { private _lastId: number; private _transport: ConnectionTransport; private readonly _protocolLogger: ProtocolLogger; @@ -83,7 +81,7 @@ export class FFConnection extends EventEmitter { this._transport.onclose = undefined; this._browserDisconnectedLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs(), reason); this.rootSession.dispose(); - Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected)); + Promise.resolve().then(() => this.emit('disconnected')); } close() { @@ -98,18 +96,13 @@ export class FFConnection extends EventEmitter { } } -export class FFSession extends EventEmitter { +export class FFSession extends ManagedEventEmitter<{ [K in keyof Protocol.Events]: [Protocol.Events[K]] }> { _connection: FFConnection; _disposed = false; private _callbacks: Map; private _sessionId: string; private _rawSend: (message: any) => void; private _crashed: boolean = false; - override on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override removeListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; constructor(connection: FFConnection, sessionId: string, rawSend: (message: any) => void) { super(); @@ -161,6 +154,7 @@ export class FFSession extends EventEmitter { } } } else { + // @ts-expect-error Promise.resolve().then(() => this.emit(object.method!, object.params)); } } diff --git a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts index 266f5bcb83..b6fe281b1c 100644 --- a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts +++ b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts @@ -38,10 +38,10 @@ export class FFNetworkManager { this._page = page; this._eventListeners = [ - eventsHelper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this)), - eventsHelper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this)), - eventsHelper.addEventListener(session, 'Network.requestFinished', this._onRequestFinished.bind(this)), - eventsHelper.addEventListener(session, 'Network.requestFailed', this._onRequestFailed.bind(this)), + session.addManagedListener('Network.requestWillBeSent', this._onRequestWillBeSent.bind(this)), + session.addManagedListener('Network.responseReceived', this._onResponseReceived.bind(this)), + session.addManagedListener('Network.requestFinished', this._onRequestFinished.bind(this)), + session.addManagedListener('Network.requestFailed', this._onRequestFailed.bind(this)), ]; } diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index aac22d5e50..59d9b40552 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -35,7 +35,6 @@ import type { Progress } from '../progress'; import { splitErrorMessage } from '../../utils/stackTrace'; import { debugLogger } from '../../utils/debugLogger'; import { ManualPromise } from '../../utils/manualPromise'; -import { BrowserContext } from '../browserContext'; import { TargetClosedError } from '../errors'; export const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -70,36 +69,36 @@ export class FFPage implements PageDelegate { this._page = new Page(this, browserContext); this.rawMouse.setPage(this._page); this._networkManager = new FFNetworkManager(session, this._page); - this._page.on(Page.Events.FrameDetached, frame => this._removeContextsForFrame(frame)); + this._page.on('framedetached', frame => this._removeContextsForFrame(frame)); // TODO: remove Page.willOpenNewWindowAsynchronously from the protocol. this._eventListeners = [ - eventsHelper.addEventListener(this._session, 'Page.eventFired', this._onEventFired.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.frameAttached', this._onFrameAttached.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.frameDetached', this._onFrameDetached.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.navigationAborted', this._onNavigationAborted.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.navigationCommitted', this._onNavigationCommitted.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.navigationStarted', this._onNavigationStarted.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)), - eventsHelper.addEventListener(this._session, 'Runtime.executionContextCreated', this._onExecutionContextCreated.bind(this)), - eventsHelper.addEventListener(this._session, 'Runtime.executionContextDestroyed', this._onExecutionContextDestroyed.bind(this)), - eventsHelper.addEventListener(this._session, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.linkClicked', event => this._onLinkClicked(event.phase)), - eventsHelper.addEventListener(this._session, 'Page.uncaughtError', this._onUncaughtError.bind(this)), - eventsHelper.addEventListener(this._session, 'Runtime.console', this._onConsole.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.fileChooserOpened', this._onFileChooserOpened.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.workerCreated', this._onWorkerCreated.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.workerDestroyed', this._onWorkerDestroyed.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.dispatchMessageFromWorker', this._onDispatchMessageFromWorker.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.crashed', this._onCrashed.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.videoRecordingStarted', this._onVideoRecordingStarted.bind(this)), + this._session.addManagedListener('Page.eventFired', this._onEventFired.bind(this)), + this._session.addManagedListener('Page.frameAttached', this._onFrameAttached.bind(this)), + this._session.addManagedListener('Page.frameDetached', this._onFrameDetached.bind(this)), + this._session.addManagedListener('Page.navigationAborted', this._onNavigationAborted.bind(this)), + this._session.addManagedListener('Page.navigationCommitted', this._onNavigationCommitted.bind(this)), + this._session.addManagedListener('Page.navigationStarted', this._onNavigationStarted.bind(this)), + this._session.addManagedListener('Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)), + this._session.addManagedListener('Runtime.executionContextCreated', this._onExecutionContextCreated.bind(this)), + this._session.addManagedListener('Runtime.executionContextDestroyed', this._onExecutionContextDestroyed.bind(this)), + this._session.addManagedListener('Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)), + this._session.addManagedListener('Page.linkClicked', event => this._onLinkClicked(event.phase)), + this._session.addManagedListener('Page.uncaughtError', this._onUncaughtError.bind(this)), + this._session.addManagedListener('Runtime.console', this._onConsole.bind(this)), + this._session.addManagedListener('Page.dialogOpened', this._onDialogOpened.bind(this)), + this._session.addManagedListener('Page.bindingCalled', this._onBindingCalled.bind(this)), + this._session.addManagedListener('Page.fileChooserOpened', this._onFileChooserOpened.bind(this)), + this._session.addManagedListener('Page.workerCreated', this._onWorkerCreated.bind(this)), + this._session.addManagedListener('Page.workerDestroyed', this._onWorkerDestroyed.bind(this)), + this._session.addManagedListener('Page.dispatchMessageFromWorker', this._onDispatchMessageFromWorker.bind(this)), + this._session.addManagedListener('Page.crashed', this._onCrashed.bind(this)), + this._session.addManagedListener('Page.videoRecordingStarted', this._onVideoRecordingStarted.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.webSocketCreated', this._onWebSocketCreated.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.webSocketClosed', this._onWebSocketClosed.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.webSocketFrameReceived', this._onWebSocketFrameReceived.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.webSocketFrameSent', this._onWebSocketFrameSent.bind(this)), - eventsHelper.addEventListener(this._session, 'Page.screencastFrame', this._onScreencastFrame.bind(this)), + this._session.addManagedListener('Page.webSocketCreated', this._onWebSocketCreated.bind(this)), + this._session.addManagedListener('Page.webSocketClosed', this._onWebSocketClosed.bind(this)), + this._session.addManagedListener('Page.webSocketFrameReceived', this._onWebSocketFrameReceived.bind(this)), + this._session.addManagedListener('Page.webSocketFrameSent', this._onWebSocketFrameSent.bind(this)), + this._session.addManagedListener('Page.screencastFrame', this._onScreencastFrame.bind(this)), ]; this._session.once('Page.ready', async () => { @@ -244,7 +243,7 @@ export class FFPage implements PageDelegate { const error = new Error(message); error.stack = params.message + '\n' + params.stack.split('\n').filter(Boolean).map(a => a.replace(/([^@]*)@(.*)/, ' at $1 ($2)')).join('\n'); error.name = name; - this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, error, this._page); + this._page.emitOnContextOnceInitialized('pageerror', error, this._page); } _onConsole(payload: Protocol.Runtime.consolePayload) { @@ -257,7 +256,7 @@ export class FFPage implements PageDelegate { } _onDialogOpened(params: Protocol.Page.dialogOpenedPayload) { - this._page.emitOnContext(BrowserContext.Events.Dialog, new dialog.Dialog( + this._page.emitOnContext('dialog', new dialog.Dialog( this._page, params.type, params.message, @@ -519,7 +518,7 @@ export class FFPage implements PageDelegate { }); const buffer = Buffer.from(event.data, 'base64'); - this._page.emit(Page.Events.ScreencastFrame, { + this._page.emit('screencastframe', { buffer, width: event.deviceWidth, height: event.deviceHeight, diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index c5bf0ba24f..23dcda4f94 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -24,9 +24,9 @@ import { eventsHelper } from '../utils/eventsHelper'; import * as js from './javascript'; import * as network from './network'; import type { Dialog } from './dialog'; -import { Page } from './page'; +import type { Page } from './page'; import * as types from './types'; -import { BrowserContext } from './browserContext'; +import type { BrowserContext } from './browserContext'; import type { Progress } from './progress'; import { ProgressController } from './progress'; import { LongStandingScope, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime } from '../utils'; @@ -156,7 +156,7 @@ export class FrameManager { assert(!this._frames.has(frameId)); const frame = new Frame(this._page, frameId, parentFrame); this._frames.set(frameId, frame); - this._page.emit(Page.Events.FrameAttached, frame); + this._page.emit('frameattached', frame); return frame; } } @@ -297,7 +297,7 @@ export class FrameManager { route.continue(request, { isFallback: true }).catch(() => {}); return; } - this._page.emitOnContext(BrowserContext.Events.Request, request); + this._page.emitOnContext('request', request); if (route) { const r = new network.Route(request, route); if (this._page._serverRequestInterceptor?.(r, request)) @@ -313,14 +313,14 @@ export class FrameManager { requestReceivedResponse(response: network.Response) { if (response.request()._isFavicon) return; - this._page.emitOnContext(BrowserContext.Events.Response, response); + this._page.emitOnContext('response', response); } reportRequestFinished(request: network.Request, response: network.Response | null) { this._inflightRequestFinished(request); if (request._isFavicon) return; - this._page.emitOnContext(BrowserContext.Events.RequestFinished, { request, response }); + this._page.emitOnContext('requestfinished', { request, response }); } requestFailed(request: network.Request, canceled: boolean) { @@ -334,7 +334,7 @@ export class FrameManager { } if (request._isFavicon) return; - this._page.emitOnContext(BrowserContext.Events.RequestFailed, request); + this._page.emitOnContext('requestfailed', request); } dialogDidOpen(dialog: Dialog) { @@ -370,7 +370,7 @@ export class FrameManager { frame._onDetached(); this._frames.delete(frame._id); if (!this._page.isClosed()) - this._page.emit(Page.Events.FrameDetached, frame); + this._page.emit('framedetached', frame); } private _inflightRequestFinished(request: network.Request) { @@ -420,7 +420,7 @@ export class FrameManager { onWebSocketRequest(requestId: string) { const ws = this._webSockets.get(requestId); if (ws && ws.markAsNotified()) - this._page.emit(Page.Events.WebSocket, ws); + this._page.emit('websocket', ws); } onWebSocketResponse(requestId: string, status: number, statusText: string) { @@ -457,17 +457,15 @@ export class FrameManager { } private _fireInternalFrameNavigation(frame: Frame, event: NavigationEvent) { - frame.emit(Frame.Events.InternalNavigation, event); + frame.emit('internalnavigation', event); } } -export class Frame extends SdkObject { - static Events = { - InternalNavigation: 'internalnavigation', - AddLifecycle: 'addlifecycle', - RemoveLifecycle: 'removelifecycle', - }; - +export class Frame extends SdkObject<{ + internalnavigation: [NavigationEvent] + addlifecycle: [types.LifecycleEvent] + removelifecycle: [types.LifecycleEvent] +}> { _id: string; _firedLifecycleEvents = new Set(); private _firedNetworkIdleSelf = false; @@ -517,7 +515,7 @@ export class Frame extends SdkObject { if (this._firedLifecycleEvents.has(event)) return; this._firedLifecycleEvents.add(event); - this.emit(Frame.Events.AddLifecycle, event); + this.emit('addlifecycle', event); if (this === this._page.mainFrame() && this._url !== 'about:blank') debugLogger.log('api', ` "${event}" event fired`); this._page.mainFrame()._recalculateNetworkIdle(); @@ -525,7 +523,7 @@ export class Frame extends SdkObject { _onClearLifecycle() { for (const event of this._firedLifecycleEvents) - this.emit(Frame.Events.RemoveLifecycle, event); + this.emit('removelifecycle', event); this._firedLifecycleEvents.clear(); // Keep the current navigation request if any. this._inflightRequests = new Set(Array.from(this._inflightRequests).filter(request => request === this._currentDocument.request)); @@ -600,7 +598,7 @@ export class Frame extends SdkObject { } if (isNetworkIdle && !this._firedLifecycleEvents.has('networkidle')) { this._firedLifecycleEvents.add('networkidle'); - this.emit(Frame.Events.AddLifecycle, 'networkidle'); + this.emit('addlifecycle', 'networkidle'); if (this === this._page.mainFrame() && this._url !== 'about:blank') debugLogger.log('api', ` "networkidle" event fired`); } @@ -608,7 +606,7 @@ export class Frame extends SdkObject { // Usually, networkidle is fired once and not removed after that. // However, when we clear them right before a new commit, this is allowed for a particular frame. this._firedLifecycleEvents.delete('networkidle'); - this.emit(Frame.Events.RemoveLifecycle, 'networkidle'); + this.emit('removelifecycle', 'networkidle'); } } @@ -661,13 +659,13 @@ export class Frame extends SdkObject { } url = helper.completeUserURL(url); - const sameDocument = helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (e: NavigationEvent) => !e.newDocument); + const sameDocument = helper.waitForEvent(progress, this, 'internalnavigation', (e: NavigationEvent) => !e.newDocument); const navigateResult = await this._page._delegate.navigateFrame(this, url, referer); let event: NavigationEvent; if (navigateResult.newDocumentId) { sameDocument.dispose(); - event = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (event: NavigationEvent) => { + event = await helper.waitForEvent(progress, this, 'internalnavigation', (event: NavigationEvent) => { // We are interested either in this specific document, or any other document that // did commit and replaced the expected document. return event.newDocument && (event.newDocument.documentId === navigateResult.newDocumentId || !event.error); @@ -685,7 +683,7 @@ export class Frame extends SdkObject { } if (!this._firedLifecycleEvents.has(waitUntil)) - await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise; + await helper.waitForEvent(progress, this, 'addlifecycle', (e: types.LifecycleEvent) => e === waitUntil).promise; const request = event.newDocument ? event.newDocument.request : undefined; const response = request ? request._finalRequest().response() : null; @@ -696,7 +694,7 @@ export class Frame extends SdkObject { const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); progress.log(`waiting for navigation until "${waitUntil}"`); - const navigationEvent: NavigationEvent = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (event: NavigationEvent) => { + const navigationEvent: NavigationEvent = await helper.waitForEvent(progress, this, 'internalnavigation', (event: NavigationEvent) => { // Any failed navigation results in a rejection. if (event.error) return true; @@ -709,7 +707,7 @@ export class Frame extends SdkObject { throw navigationEvent.error; if (!this._firedLifecycleEvents.has(waitUntil)) - await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise; + await helper.waitForEvent(progress, this, 'addlifecycle', (e: types.LifecycleEvent) => e === waitUntil).promise; const request = navigationEvent.newDocument ? navigationEvent.newDocument.request : undefined; return request ? request._finalRequest().response() : null; @@ -718,7 +716,7 @@ export class Frame extends SdkObject { async _waitForLoadState(progress: Progress, state: types.LifecycleEvent): Promise { const waitUntil = verifyLifecycle('state', state); if (!this._firedLifecycleEvents.has(waitUntil)) - await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise; + await helper.waitForEvent(progress, this, 'addlifecycle', (e: types.LifecycleEvent) => e === waitUntil).promise; } async frameElement(): Promise { @@ -1034,7 +1032,7 @@ export class Frame extends SdkObject { let cspMessage: ConsoleMessage | undefined; const actionPromise = func().then(r => result = r).catch(e => error = e); const errorPromise = new Promise(resolve => { - listeners.push(eventsHelper.addEventListener(this._page._browserContext, BrowserContext.Events.Console, (message: ConsoleMessage) => { + listeners.push(this._page._browserContext.addManagedListener('console', (message: ConsoleMessage) => { if (message.page() !== this._page || message.type() !== 'error') return; if (message.text().includes('Content-Security-Policy') || message.text().includes('Content Security Policy')) { @@ -1725,7 +1723,7 @@ class SignalBarrier { if (frame.parentFrame()) return; this.retain(); - const waiter = helper.waitForEvent(null, frame, Frame.Events.InternalNavigation, (e: NavigationEvent) => { + const waiter = helper.waitForEvent(null, frame, 'internalnavigation', (e: NavigationEvent) => { if (!e.isPublic) return false; if (!e.error && this._progress) diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index cb423a144d..8c22e5ccd2 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -29,7 +29,6 @@ import { mime } from '../../utilsBundle'; import { ManualPromise } from '../../utils/manualPromise'; import { getPlaywrightVersion } from '../../utils/userAgent'; import { urlMatches } from '../../utils/network'; -import { Frame } from '../frames'; import type { HeadersArray, LifecycleEvent } from '../types'; import { isTextualMimeType } from '../../utils/isomorphic/mimeType'; @@ -93,19 +92,19 @@ export class HarTracer { this._started = true; const apiRequest = this._context instanceof APIRequestContext ? this._context : this._context.fetchRequest; this._eventListeners = [ - eventsHelper.addEventListener(apiRequest, APIRequestContext.Events.Request, (event: APIRequestEvent) => this._onAPIRequest(event)), - eventsHelper.addEventListener(apiRequest, APIRequestContext.Events.RequestFinished, (event: APIRequestFinishedEvent) => this._onAPIRequestFinished(event)), + apiRequest.addManagedListener('request', (event: APIRequestEvent) => this._onAPIRequest(event)), + apiRequest.addManagedListener('requestfinished', (event: APIRequestFinishedEvent) => this._onAPIRequestFinished(event)), ]; if (this._context instanceof BrowserContext) { this._eventListeners.push( - eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, (page: Page) => this._createPageEntryIfNeeded(page)), - eventsHelper.addEventListener(this._context, BrowserContext.Events.Request, (request: network.Request) => this._onRequest(request)), - eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFinished, ({ request, response }) => this._onRequestFinished(request, response).catch(() => {})), - eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFailed, request => this._onRequestFailed(request)), - eventsHelper.addEventListener(this._context, BrowserContext.Events.Response, (response: network.Response) => this._onResponse(response)), - eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestAborted, request => this._onRequestAborted(request)), - eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFulfilled, request => this._onRequestFulfilled(request)), - eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestContinued, request => this._onRequestContinued(request)), + this._context.addManagedListener('page', (page: Page) => this._createPageEntryIfNeeded(page)), + this._context.addManagedListener('request', (request: network.Request) => this._onRequest(request)), + this._context.addManagedListener('requestfinished', ({ request, response }) => this._onRequestFinished(request, response).catch(() => {})), + this._context.addManagedListener('requestfailed', request => this._onRequestFailed(request)), + this._context.addManagedListener('response', (response: network.Response) => this._onResponse(response)), + this._context.addManagedListener('requestaborted', request => this._onRequestAborted(request)), + this._context.addManagedListener('requestfulfilled', request => this._onRequestFulfilled(request)), + this._context.addManagedListener('requestcontinued', request => this._onRequestContinued(request)), ); } } @@ -139,7 +138,7 @@ export class HarTracer { }; (pageEntry as any)[startedDateSymbol] = date; - page.mainFrame().on(Frame.Events.AddLifecycle, (event: LifecycleEvent) => { + page.mainFrame().on('addlifecycle', (event: LifecycleEvent) => { if (event === 'load') this._onLoad(page, pageEntry!); if (event === 'domcontentloaded') diff --git a/packages/playwright-core/src/server/helper.ts b/packages/playwright-core/src/server/helper.ts index 5b22dfd5e1..ff6964af89 100644 --- a/packages/playwright-core/src/server/helper.ts +++ b/packages/playwright-core/src/server/helper.ts @@ -15,11 +15,10 @@ * limitations under the License. */ -import type { EventEmitter } from 'events'; import type * as types from './types'; import type { Progress } from './progress'; import { debugLogger } from '../utils/debugLogger'; -import type { RegisteredListener } from '../utils/eventsHelper'; +import type { ManagedEventEmitter, RegisteredListener } from '../utils/eventsHelper'; import { eventsHelper } from '../utils/eventsHelper'; const MAX_LOG_LENGTH = process.env.MAX_LOG_LENGTH ? +process.env.MAX_LOG_LENGTH : Infinity; @@ -53,10 +52,10 @@ class Helper { return null; } - static waitForEvent(progress: Progress | null, emitter: EventEmitter, event: string | symbol, predicate?: Function): { promise: Promise, dispose: () => void } { + static waitForEvent, K extends keyof T['_events']>(progress: Progress | null, emitter: T, event: K, predicate?: Function): { promise: Promise, dispose: () => void } { const listeners: RegisteredListener[] = []; const promise = new Promise((resolve, reject) => { - listeners.push(eventsHelper.addEventListener(emitter, event, eventArg => { + listeners.push(emitter.addManagedListener(event, eventArg => { try { if (predicate && !predicate(eventArg)) return; diff --git a/packages/playwright-core/src/server/instrumentation.ts b/packages/playwright-core/src/server/instrumentation.ts index b4628ac904..e41bb54853 100644 --- a/packages/playwright-core/src/server/instrumentation.ts +++ b/packages/playwright-core/src/server/instrumentation.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { EventEmitter } from 'events'; -import { createGuid } from '../utils'; +import { createGuid, ManagedEventEmitter } from '../utils'; import type { APIRequestContext } from './fetch'; import type { Browser } from './browser'; import type { BrowserContext } from './browserContext'; @@ -37,7 +36,7 @@ export type Attribution = { import type { CallMetadata } from '@protocol/callMetadata'; export type { CallMetadata } from '@protocol/callMetadata'; -export class SdkObject extends EventEmitter { +export class SdkObject = any> extends ManagedEventEmitter { guid: string; attribution: Attribution; instrumentation: Instrumentation; diff --git a/packages/playwright-core/src/server/network.ts b/packages/playwright-core/src/server/network.ts index fd62e1751b..0f9ece2f4b 100644 --- a/packages/playwright-core/src/server/network.ts +++ b/packages/playwright-core/src/server/network.ts @@ -25,7 +25,6 @@ import { SdkObject } from './instrumentation'; import type { HeadersArray, NameValue } from '../common/types'; import { APIRequestContext } from './fetch'; import type { NormalizedContinueOverrides } from './types'; -import { BrowserContext } from './browserContext'; export function filterCookies(cookies: channels.NetworkCookie[], urls: string[]): channels.NetworkCookie[] { const parsedURLs = urls.map(s => new URL(s)); @@ -257,7 +256,7 @@ export class Route extends SdkObject { async abort(errorCode: string = 'failed') { this._startHandling(); - this._request._context.emit(BrowserContext.Events.RequestAborted, this._request); + this._request._context.emit('requestaborted', this._request); await this._delegate.abort(errorCode); this._endHandling(); } @@ -285,7 +284,7 @@ export class Route extends SdkObject { } const headers = [...(overrides.headers || [])]; this._maybeAddCorsHeaders(headers); - this._request._context.emit(BrowserContext.Events.RequestFulfilled, this._request); + this._request._context.emit('requestfulfilled', this._request); await this._delegate.fulfill({ status: overrides.status || 200, headers, @@ -323,7 +322,7 @@ export class Route extends SdkObject { } this._request._setOverrides(overrides); if (!overrides.isFallback) - this._request._context.emit(BrowserContext.Events.RequestContinued, this._request); + this._request._context.emit('requestcontinued', this._request); await this._delegate.continue(this._request, overrides); this._endHandling(); } @@ -562,17 +561,15 @@ export class Response extends SdkObject { } } -export class WebSocket extends SdkObject { +export class WebSocket extends SdkObject<{ + close: [] + socketerror: [string] + framereceived: [{ opcode: number, data: string }] + framesent: [{ opcode: number, data: string }] +}> { private _url: string; private _notified = false; - static Events = { - Close: 'close', - SocketError: 'socketerror', - FrameReceived: 'framereceived', - FrameSent: 'framesent', - }; - constructor(parent: SdkObject, url: string) { super(parent, 'ws'); this._url = url; @@ -593,19 +590,19 @@ export class WebSocket extends SdkObject { } frameSent(opcode: number, data: string) { - this.emit(WebSocket.Events.FrameSent, { opcode, data }); + this.emit('framesent', { opcode, data }); } frameReceived(opcode: number, data: string) { - this.emit(WebSocket.Events.FrameReceived, { opcode, data }); + this.emit('framereceived', { opcode, data }); } error(errorMessage: string) { - this.emit(WebSocket.Events.SocketError, errorMessage); + this.emit('socketerror', errorMessage); } closed() { - this.emit(WebSocket.Events.Close); + this.emit('close'); } } diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 6206681048..286975275c 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -25,7 +25,8 @@ import type { ScreenshotOptions } from './screenshotter'; import { Screenshotter, validateScreenshotOptions } from './screenshotter'; import { TimeoutSettings } from '../common/timeoutSettings'; import type * as types from './types'; -import { BrowserContext } from './browserContext'; +import type { BrowserContextEvents } from './browserContext'; +import type { BrowserContext } from './browserContext'; import { ConsoleMessage } from './console'; import * as accessibility from './accessibility'; import { FileChooser } from './fileChooser'; @@ -45,6 +46,7 @@ import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSe import type { SerializedValue } from './isomorphic/utilityScriptSerializers'; import { TargetClosedError } from './errors'; import { asLocator } from '../utils/isomorphic/locatorGenerators'; +import type { Download } from './download'; export interface PageDelegate { readonly rawMouse: input.RawMouse; @@ -121,26 +123,24 @@ type ExpectScreenshotOptions = ImageComparatorOptions & ScreenshotOptions & { }, }; -export class Page extends SdkObject { - static Events = { - Close: 'close', - Crash: 'crash', - Download: 'download', - FileChooser: 'filechooser', - FrameAttached: 'frameattached', - FrameDetached: 'framedetached', - InternalFrameNavigatedToNewDocument: 'internalframenavigatedtonewdocument', - LocatorHandlerTriggered: 'locatorhandlertriggered', - ScreencastFrame: 'screencastframe', - Video: 'video', - WebSocket: 'websocket', - Worker: 'worker', - }; - +export class Page extends SdkObject<{ + close: [] + crash: [] + download: [Download] + filechooser: [FileChooser] + frameattached: [frames.Frame] + framedetached: [frames.Frame] + internalframenavigatedtonewdocument: [frames.Frame] + locatorhandlertriggered: [number] + screencastframe: [{ buffer: Buffer, width: number, height: number, timestamp?: number }] + video: [Artifact] + websocket: [network.WebSocket] + worker: [Worker] +}> { private _closedState: 'open' | 'closing' | 'closed' = 'open'; private _closedPromise = new ManualPromise(); private _initialized = false; - private _eventsToEmitAfterInitialized: { event: string | symbol, args: any[] }[] = []; + private _eventsToEmitAfterInitialized: { event: string, args: any[] }[] = []; private _crashed = false; readonly openScope = new LongStandingScope(); readonly _browserContext: BrowserContext; @@ -202,7 +202,7 @@ export class Page extends SdkObject { this._opener = openerPage; } - reportAsNew(error: Error | undefined = undefined, contextEvent: string = BrowserContext.Events.Page) { + reportAsNew(error: Error | undefined = undefined, contextEvent: keyof BrowserContext['_events'] = 'page') { if (error) { // Initialization error could have happened because of // context/browser closure. Just ignore the page. @@ -214,6 +214,7 @@ export class Page extends SdkObject { this.emitOnContext(contextEvent, this); for (const { event, args } of this._eventsToEmitAfterInitialized) + // @ts-ignore this._browserContext.emit(event, ...args); this._eventsToEmitAfterInitialized = []; @@ -221,7 +222,7 @@ export class Page extends SdkObject { // in that case we fire another Close event to ensure that each reported Page will have // corresponding Close event after it is reported on the context. if (this.isClosed()) - this.emit(Page.Events.Close); + this.emit('close'); else this.instrumentation.onPageOpen(this); } @@ -230,13 +231,14 @@ export class Page extends SdkObject { return this._initialized ? this : undefined; } - emitOnContext(event: string | symbol, ...args: any[]) { + emitOnContext(event: K, ...args: BrowserContextEvents[K]) { if (this._isServerSideOnly) return; + // @ts-ignore this._browserContext.emit(event, ...args); } - emitOnContextOnceInitialized(event: string | symbol, ...args: any[]) { + emitOnContextOnceInitialized(event: K, ...args: BrowserContextEvents[K]) { if (this._isServerSideOnly) return; // Some events, like console messages, may come before page is ready. @@ -244,6 +246,7 @@ export class Page extends SdkObject { // and dispatch it to the client later, either on the live Page, // or on the "errored" Page. if (this._initialized) + // @ts-ignore this._browserContext.emit(event, ...args); else this._eventsToEmitAfterInitialized.push({ event, args }); @@ -280,7 +283,7 @@ export class Page extends SdkObject { this._frameThrottler.dispose(); assert(this._closedState !== 'closed', 'Page closed twice'); this._closedState = 'closed'; - this.emit(Page.Events.Close); + this.emit('close'); this._closedPromise.resolve(); this.instrumentation.onPageClose(this); this.openScope.close(new TargetClosedError()); @@ -289,7 +292,7 @@ export class Page extends SdkObject { _didCrash() { this._frameManager.dispose(); this._frameThrottler.dispose(); - this.emit(Page.Events.Crash); + this.emit('crash'); this._crashed = true; this.instrumentation.onPageClose(this); this.openScope.close(new Error('Page crashed')); @@ -303,12 +306,12 @@ export class Page extends SdkObject { // Frame/context may be gone during async processing. Do not throw. return; } - if (!this.listenerCount(Page.Events.FileChooser)) { + if (!this.listenerCount('filechooser')) { handle.dispose(); return; } const fileChooser = new FileChooser(this, handle, multiple); - this.emit(Page.Events.FileChooser, fileChooser); + this.emit('filechooser', fileChooser); } context(): BrowserContext { @@ -375,7 +378,7 @@ export class Page extends SdkObject { args.forEach(arg => arg.dispose()); return; } - this.emitOnContextOnceInitialized(BrowserContext.Events.Console, message); + this.emitOnContextOnceInitialized('console', message); } async reload(metadata: CallMetadata, options: types.NavigateOptions): Promise { @@ -460,7 +463,7 @@ export class Page extends SdkObject { if (!handler.resolved) { if (await this.mainFrame().isVisibleInternal(handler.selector, { strict: true })) { handler.resolved = new ManualPromise(); - this.emit(Page.Events.LocatorHandlerTriggered, uid); + this.emit('locatorhandlertriggered', uid); } } if (handler.resolved) { @@ -693,7 +696,7 @@ export class Page extends SdkObject { _addWorker(workerId: string, worker: Worker) { this._workers.set(workerId, worker); - this.emit(Page.Events.Worker, worker); + this.emit('worker', worker); } _removeWorker(workerId: string) { @@ -721,7 +724,7 @@ export class Page extends SdkObject { } frameNavigatedToNewDocument(frame: frames.Frame) { - this.emit(Page.Events.InternalFrameNavigatedToNewDocument, frame); + this.emit('internalframenavigatedtonewdocument', frame); const origin = frame.origin(); if (origin) this._browserContext.addVisitedOrigin(origin); @@ -769,11 +772,9 @@ export class Page extends SdkObject { } } -export class Worker extends SdkObject { - static Events = { - Close: 'close', - }; - +export class Worker extends SdkObject<{ + close: [Worker] +}> { private _url: string; private _executionContextPromise: Promise; private _executionContextCallback: (value: js.ExecutionContext) => void; @@ -799,7 +800,7 @@ export class Worker extends SdkObject { didClose() { if (this._existingExecutionContext) this._existingExecutionContext.contextDestroyed('Worker was closed'); - this.emit(Worker.Events.Close, this); + this.emit('close', this); this.openScope.close(new Error('Worker closed')); } diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 3a770741ae..90e0cbd37a 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -20,9 +20,9 @@ import type * as channels from '@protocol/channels'; import type { ActionInContext } from './recorder/codeGenerator'; import { CodeGenerator } from './recorder/codeGenerator'; import { toClickOptions, toModifiers } from './recorder/utils'; -import { Page } from './page'; -import { Frame } from './frames'; -import { BrowserContext } from './browserContext'; +import type { Page } from './page'; +import type { Frame } from './frames'; +import type { BrowserContext } from './browserContext'; import { JavaLanguageGenerator } from './recorder/java'; import { JavaScriptLanguageGenerator } from './recorder/javascript'; import { JsonlLanguageGenerator } from './recorder/jsonl'; @@ -38,7 +38,7 @@ import type { Point } from '../common/types'; import type { CallLog, CallLogStatus, EventData, Mode, OverlayState, Source, UIState } from '@recorder/recorderTypes'; import { createGuid, isUnderTest, monotonicTime } from '../utils'; import { metadataToCallLog } from './recorder/recorderUtils'; -import { Debugger } from './debugger'; +import type { Debugger } from './debugger'; import { EventEmitter } from 'events'; import { raceAgainstDeadline } from '../utils/timeoutRunner'; import type { Language, LanguageGenerator } from './recorder/language'; @@ -156,12 +156,12 @@ export class Recorder implements InstrumentationListener { this._pushAllSources() ]); - this._context.once(BrowserContext.Events.Close, () => { + this._context.once('close', () => { this._contextRecorder.dispose(); this._context.instrumentation.removeListener(this); recorderApp.close().catch(() => {}); }); - this._contextRecorder.on(ContextRecorder.Events.Change, (data: { sources: Source[], primaryFileName: string }) => { + this._contextRecorder.on('change', data => { this._recorderSources = data.sources; this._pushAllSources(); this._recorderApp?.setFileIfNeeded(data.primaryFileName); @@ -224,7 +224,7 @@ export class Recorder implements InstrumentationListener { if (this._debugger.isPaused()) this._pausedStateChanged(); - this._debugger.on(Debugger.Events.PausedStateChanged, () => this._pausedStateChanged()); + this._debugger.on('pausedstatechanged', () => this._pausedStateChanged()); (this._context as any).recorderAppForTest = recorderApp; } @@ -370,11 +370,9 @@ export class Recorder implements InstrumentationListener { } } -class ContextRecorder extends EventEmitter { - static Events = { - Change: 'change' - }; - +class ContextRecorder extends EventEmitter<{ + change: [{ sources: Source[], primaryFileName: string }] +}> { private _generator: CodeGenerator; private _pageAliases = new Map(); private _lastPopupOrdinal = 0; @@ -417,12 +415,12 @@ class ContextRecorder extends EventEmitter { if (languageGenerator === this._orderedLanguages[0]) this._throttledOutputFile?.setContent(source.text); } - this.emit(ContextRecorder.Events.Change, { + this.emit('change', { sources: this._recorderSources, primaryFileName: this._orderedLanguages[0].id }); }); - context.on(BrowserContext.Events.BeforeClose, () => { + context.on('beforeclose', () => { this._throttledOutputFile?.flush(); }); this._listeners.push(eventsHelper.addEventListener(process, 'exit', () => { @@ -463,10 +461,10 @@ class ContextRecorder extends EventEmitter { } async install() { - this._context.on(BrowserContext.Events.Page, (page: Page) => this._onPage(page)); + this._context.on('page', (page: Page) => this._onPage(page)); for (const page of this._context.pages()) this._onPage(page); - this._context.on(BrowserContext.Events.Dialog, (dialog: Dialog) => this._onDialog(dialog.page())); + this._context.on('dialog', (dialog: Dialog) => this._onDialog(dialog.page())); // Input actions that potentially lead to navigation are intercepted on the page and are // performed by the Playwright. @@ -505,11 +503,11 @@ class ContextRecorder extends EventEmitter { }); this._pageAliases.delete(page); }); - frame.on(Frame.Events.InternalNavigation, event => { + frame.on('internalnavigation', event => { if (event.isPublic) this._onFrameNavigated(frame, page); }); - page.on(Page.Events.Download, () => this._onDownload(page)); + page.on('download', () => this._onDownload(page)); const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : ''; const pageAlias = 'page' + suffix; this._pageAliases.set(page, pageAlias); diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts index 5c483e7c30..a493affce3 100644 --- a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -139,7 +139,7 @@ export class ClientCertificatesProxy { ) { this._socksProxy = new SocksProxy(); this._socksProxy.setPattern('*'); - this._socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => { + this._socksProxy.addListener('socksRequested', async (payload: SocksSocketRequestedPayload) => { try { const connection = new SocksProxyConnection(this, payload.uid, payload.host, payload.port); await connection.connect(); @@ -148,10 +148,10 @@ export class ClientCertificatesProxy { this._socksProxy.socketFailed({ uid: payload.uid, errorCode: error.code }); } }); - this._socksProxy.addListener(SocksProxy.Events.SocksData, async (payload: SocksSocketDataPayload) => { + this._socksProxy.addListener('socksData', async (payload: SocksSocketDataPayload) => { this._connections.get(payload.uid)?.onData(payload.data); }); - this._socksProxy.addListener(SocksProxy.Events.SocksClosed, (payload: SocksSocketClosedPayload) => { + this._socksProxy.addListener('socksClosed', (payload: SocksSocketClosedPayload) => { this._connections.get(payload.uid)?.onClose(); this._connections.delete(payload.uid); }); diff --git a/packages/playwright-core/src/server/socksInterceptor.ts b/packages/playwright-core/src/server/socksInterceptor.ts index 498e8bfe73..ff9f282e76 100644 --- a/packages/playwright-core/src/server/socksInterceptor.ts +++ b/packages/playwright-core/src/server/socksInterceptor.ts @@ -47,11 +47,11 @@ export class SocksInterceptor { }; }, }) as channels.SocksSupportChannel & EventEmitter; - this._handler.on(socks.SocksProxyHandler.Events.SocksConnected, (payload: socks.SocksSocketConnectedPayload) => this._channel.socksConnected(payload)); - this._handler.on(socks.SocksProxyHandler.Events.SocksData, (payload: socks.SocksSocketDataPayload) => this._channel.socksData(payload)); - this._handler.on(socks.SocksProxyHandler.Events.SocksError, (payload: socks.SocksSocketErrorPayload) => this._channel.socksError(payload)); - this._handler.on(socks.SocksProxyHandler.Events.SocksFailed, (payload: socks.SocksSocketFailedPayload) => this._channel.socksFailed(payload)); - this._handler.on(socks.SocksProxyHandler.Events.SocksEnd, (payload: socks.SocksSocketEndPayload) => this._channel.socksEnd(payload)); + this._handler.on('socksConnected', (payload: socks.SocksSocketConnectedPayload) => this._channel.socksConnected(payload)); + this._handler.on('socksData', (payload: socks.SocksSocketDataPayload) => this._channel.socksData(payload)); + this._handler.on('socksError', (payload: socks.SocksSocketErrorPayload) => this._channel.socksError(payload)); + this._handler.on('socksFailed', (payload: socks.SocksSocketFailedPayload) => this._channel.socksFailed(payload)); + this._handler.on('socksEnd', (payload: socks.SocksSocketEndPayload) => this._channel.socksEnd(payload)); this._channel.on('socksRequested', payload => this._handler.socketRequested(payload)); this._channel.on('socksClosed', payload => this._handler.socketClosed(payload)); this._channel.on('socksData', payload => this._handler.sendSocketData(payload)); diff --git a/packages/playwright-core/src/server/trace/recorder/snapshotter.ts b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts index 48bfea7302..1812b0bc5b 100644 --- a/packages/playwright-core/src/server/trace/recorder/snapshotter.ts +++ b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { BrowserContext } from '../../browserContext'; -import { Page } from '../../page'; +import type { BrowserContext } from '../../browserContext'; +import type { Page } from '../../page'; import type { RegisteredListener } from '../../../utils/eventsHelper'; import { eventsHelper } from '../../../utils/eventsHelper'; import { debugLogger } from '../../../utils/debugLogger'; @@ -83,7 +83,7 @@ export class Snapshotter { for (const page of this._context.pages()) this._onPage(page); this._eventListeners = [ - eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, this._onPage.bind(this)), + this._context.addManagedListener('page', this._onPage.bind(this)), ]; const { javaScriptEnabled } = this._context._options; @@ -160,7 +160,7 @@ export class Snapshotter { // Annotate frame hierarchy so that snapshots could include frame ids. for (const frame of page.frames()) this._annotateFrameHierarchy(frame); - this._eventListeners.push(eventsHelper.addEventListener(page, Page.Events.FrameAttached, frame => this._annotateFrameHierarchy(frame))); + this._eventListeners.push(page.addManagedListener('frameattached', frame => this._annotateFrameHierarchy(frame))); } private async _annotateFrameHierarchy(frame: Frame) { diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 1dec4f536a..8ab55eb4ba 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -27,7 +27,7 @@ import type { ElementHandle } from '../../dom'; import type { APIRequestContext } from '../../fetch'; import type { CallMetadata, InstrumentationListener } from '../../instrumentation'; import { SdkObject } from '../../instrumentation'; -import { Page } from '../../page'; +import type { Page } from '../../page'; import type * as har from '@trace/har'; import type { HarTracerDelegate } from '../../har/harTracer'; import { HarTracer } from '../../har/harTracer'; @@ -182,10 +182,12 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this._fs.appendFile(this._state.traceFile, JSON.stringify(event) + '\n'); this._context.instrumentation.addListener(this, this._context); - this._eventListeners.push( - eventsHelper.addEventListener(this._context, BrowserContext.Events.Console, this._onConsoleMessage.bind(this)), - eventsHelper.addEventListener(this._context, BrowserContext.Events.PageError, this._onPageError.bind(this)), - ); + if (this._context instanceof BrowserContext) { + this._eventListeners.push( + this._context.addManagedListener('console', this._onConsoleMessage.bind(this)), + this._context.addManagedListener('pageerror', this._onPageError.bind(this)), + ); + } if (this._state.options.screenshots) this._startScreencast(); if (this._state.options.snapshots) @@ -199,7 +201,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps for (const page of this._context.pages()) this._startScreencastInPage(page); this._screencastListeners.push( - eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, this._startScreencastInPage.bind(this)), + this._context.addManagedListener('page', this._startScreencastInPage.bind(this)), ); } @@ -463,7 +465,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps page.setScreencastOptions(kScreencastOptions); const prefix = page.guid; this._screencastListeners.push( - eventsHelper.addEventListener(page, Page.Events.ScreencastFrame, params => { + page.addManagedListener('screencastframe', params => { const suffix = params.timestamp || Date.now(); const sha1 = `${prefix}-${suffix}.jpeg`; const event: trace.ScreencastFrameTraceEvent = { diff --git a/packages/playwright-core/src/server/webkit/wkBrowser.ts b/packages/playwright-core/src/server/webkit/wkBrowser.ts index 9eedec410b..74055d5a30 100644 --- a/packages/playwright-core/src/server/webkit/wkBrowser.ts +++ b/packages/playwright-core/src/server/webkit/wkBrowser.ts @@ -20,7 +20,6 @@ import { Browser } from '../browser'; import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext'; import type { RegisteredListener } from '../../utils/eventsHelper'; import { assert } from '../../utils'; -import { eventsHelper } from '../../utils/eventsHelper'; import * as network from '../network'; import type { InitScript, Page, PageBinding, PageDelegate } from '../page'; import type { ConnectionTransport } from '../transport'; @@ -64,15 +63,15 @@ export class WKBrowser extends Browser { this._connection = new WKConnection(transport, this._onDisconnect.bind(this), options.protocolLogger, options.browserLogsCollector); this._browserSession = this._connection.browserSession; this._eventListeners = [ - eventsHelper.addEventListener(this._browserSession, 'Playwright.pageProxyCreated', this._onPageProxyCreated.bind(this)), - eventsHelper.addEventListener(this._browserSession, 'Playwright.pageProxyDestroyed', this._onPageProxyDestroyed.bind(this)), - eventsHelper.addEventListener(this._browserSession, 'Playwright.provisionalLoadFailed', event => this._onProvisionalLoadFailed(event)), - eventsHelper.addEventListener(this._browserSession, 'Playwright.windowOpen', event => this._onWindowOpen(event)), - eventsHelper.addEventListener(this._browserSession, 'Playwright.downloadCreated', this._onDownloadCreated.bind(this)), - eventsHelper.addEventListener(this._browserSession, 'Playwright.downloadFilenameSuggested', this._onDownloadFilenameSuggested.bind(this)), - eventsHelper.addEventListener(this._browserSession, 'Playwright.downloadFinished', this._onDownloadFinished.bind(this)), - eventsHelper.addEventListener(this._browserSession, 'Playwright.screencastFinished', this._onScreencastFinished.bind(this)), - eventsHelper.addEventListener(this._browserSession, kPageProxyMessageReceived, this._onPageProxyMessageReceived.bind(this)), + this._browserSession.addManagedListener('Playwright.pageProxyCreated', this._onPageProxyCreated.bind(this)), + this._browserSession.addManagedListener('Playwright.pageProxyDestroyed', this._onPageProxyDestroyed.bind(this)), + this._browserSession.addManagedListener('Playwright.provisionalLoadFailed', event => this._onProvisionalLoadFailed(event)), + this._browserSession.addManagedListener('Playwright.windowOpen', event => this._onWindowOpen(event)), + this._browserSession.addManagedListener('Playwright.downloadCreated', this._onDownloadCreated.bind(this)), + this._browserSession.addManagedListener('Playwright.downloadFilenameSuggested', this._onDownloadFilenameSuggested.bind(this)), + this._browserSession.addManagedListener('Playwright.downloadFinished', this._onDownloadFinished.bind(this)), + this._browserSession.addManagedListener('Playwright.screencastFinished', this._onScreencastFinished.bind(this)), + this._browserSession.addManagedListener(kPageProxyMessageReceived, this._onPageProxyMessageReceived.bind(this)), ]; } diff --git a/packages/playwright-core/src/server/webkit/wkConnection.ts b/packages/playwright-core/src/server/webkit/wkConnection.ts index 403b32bbd6..9143a4b96d 100644 --- a/packages/playwright-core/src/server/webkit/wkConnection.ts +++ b/packages/playwright-core/src/server/webkit/wkConnection.ts @@ -15,8 +15,7 @@ * limitations under the License. */ -import { EventEmitter } from 'events'; -import { assert } from '../../utils'; +import { assert, ManagedEventEmitter } from '../../utils'; import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import type { Protocol } from './protocol'; import type { RecentLogsCollector } from '../../utils/debugLogger'; @@ -97,7 +96,11 @@ export class WKConnection { } } -export class WKSession extends EventEmitter { +export class WKSession extends ManagedEventEmitter<{ + [K in keyof Protocol.Events]: [Protocol.Events[K]] + } & { + [kPageProxyMessageReceived]: [PageProxyMessageReceivedPayload] + }> { connection: WKConnection; readonly sessionId: string; @@ -106,12 +109,6 @@ export class WKSession extends EventEmitter { private readonly _callbacks = new Map void, reject: (e: ProtocolError) => void, error: ProtocolError }>(); private _crashed: boolean = false; - override on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override removeListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - constructor(connection: WKConnection, sessionId: string, rawSend: (message: any) => void) { super(); this.setMaxListeners(0); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index adfd06c05e..fca1c8bfa7 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -45,7 +45,6 @@ import { WKProvisionalPage } from './wkProvisionalPage'; import { WKWorkers } from './wkWorkers'; import { debugLogger } from '../../utils/debugLogger'; import { ManualPromise } from '../../utils/manualPromise'; -import { BrowserContext } from '../browserContext'; import { TargetClosedError } from '../errors'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -92,13 +91,13 @@ export class WKPage implements PageDelegate { this._workers = new WKWorkers(this._page); this._session = undefined as any as WKSession; this._browserContext = browserContext; - this._page.on(Page.Events.FrameDetached, (frame: frames.Frame) => this._removeContextsForFrame(frame, false)); + this._page.on('framedetached', (frame: frames.Frame) => this._removeContextsForFrame(frame, false)); this._eventListeners = [ - eventsHelper.addEventListener(this._pageProxySession, 'Target.targetCreated', this._onTargetCreated.bind(this)), - eventsHelper.addEventListener(this._pageProxySession, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)), - eventsHelper.addEventListener(this._pageProxySession, 'Target.dispatchMessageFromTarget', this._onDispatchMessageFromTarget.bind(this)), - eventsHelper.addEventListener(this._pageProxySession, 'Target.didCommitProvisionalTarget', this._onDidCommitProvisionalTarget.bind(this)), - eventsHelper.addEventListener(this._pageProxySession, 'Screencast.screencastFrame', this._onScreencastFrame.bind(this)), + this._pageProxySession.addManagedListener('Target.targetCreated', this._onTargetCreated.bind(this)), + this._pageProxySession.addManagedListener('Target.targetDestroyed', this._onTargetDestroyed.bind(this)), + this._pageProxySession.addManagedListener('Target.dispatchMessageFromTarget', this._onDispatchMessageFromTarget.bind(this)), + this._pageProxySession.addManagedListener('Target.didCommitProvisionalTarget', this._onDidCommitProvisionalTarget.bind(this)), + this._pageProxySession.addManagedListener('Screencast.screencastFrame', this._onScreencastFrame.bind(this)), ]; this._firstNonInitialNavigationCommittedPromise = new Promise((f, r) => { this._firstNonInitialNavigationCommittedFulfill = f; @@ -373,33 +372,33 @@ export class WKPage implements PageDelegate { private _addSessionListeners() { this._sessionListeners = [ - eventsHelper.addEventListener(this._session, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)), - eventsHelper.addEventListener(this._session, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)), - eventsHelper.addEventListener(this._session, 'Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId)), - eventsHelper.addEventListener(this._session, 'Page.frameDetached', event => this._onFrameDetached(event.frameId)), - eventsHelper.addEventListener(this._session, 'Page.willCheckNavigationPolicy', event => this._onWillCheckNavigationPolicy(event.frameId)), - eventsHelper.addEventListener(this._session, 'Page.didCheckNavigationPolicy', event => this._onDidCheckNavigationPolicy(event.frameId, event.cancel)), - eventsHelper.addEventListener(this._session, 'Page.frameScheduledNavigation', event => this._onFrameScheduledNavigation(event.frameId, event.delay, event.targetIsCurrentFrame)), - eventsHelper.addEventListener(this._session, 'Page.loadEventFired', event => this._page._frameManager.frameLifecycleEvent(event.frameId, 'load')), - eventsHelper.addEventListener(this._session, 'Page.domContentEventFired', event => this._page._frameManager.frameLifecycleEvent(event.frameId, 'domcontentloaded')), - eventsHelper.addEventListener(this._session, 'Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)), - eventsHelper.addEventListener(this._session, 'Runtime.bindingCalled', event => this._onBindingCalled(event.contextId, event.argument)), - eventsHelper.addEventListener(this._session, 'Console.messageAdded', event => this._onConsoleMessage(event)), - eventsHelper.addEventListener(this._session, 'Console.messageRepeatCountUpdated', event => this._onConsoleRepeatCountUpdated(event)), - eventsHelper.addEventListener(this._pageProxySession, 'Dialog.javascriptDialogOpening', event => this._onDialog(event)), - eventsHelper.addEventListener(this._session, 'Page.fileChooserOpened', event => this._onFileChooserOpened(event)), - eventsHelper.addEventListener(this._session, 'Network.requestWillBeSent', e => this._onRequestWillBeSent(this._session, e)), - eventsHelper.addEventListener(this._session, 'Network.requestIntercepted', e => this._onRequestIntercepted(this._session, e)), - eventsHelper.addEventListener(this._session, 'Network.responseReceived', e => this._onResponseReceived(this._session, e)), - eventsHelper.addEventListener(this._session, 'Network.loadingFinished', e => this._onLoadingFinished(e)), - eventsHelper.addEventListener(this._session, 'Network.loadingFailed', e => this._onLoadingFailed(this._session, e)), - eventsHelper.addEventListener(this._session, 'Network.webSocketCreated', e => this._page._frameManager.onWebSocketCreated(e.requestId, e.url)), - eventsHelper.addEventListener(this._session, 'Network.webSocketWillSendHandshakeRequest', e => this._page._frameManager.onWebSocketRequest(e.requestId)), - eventsHelper.addEventListener(this._session, 'Network.webSocketHandshakeResponseReceived', e => this._page._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)), - eventsHelper.addEventListener(this._session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)), - eventsHelper.addEventListener(this._session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)), - eventsHelper.addEventListener(this._session, 'Network.webSocketClosed', e => this._page._frameManager.webSocketClosed(e.requestId)), - eventsHelper.addEventListener(this._session, 'Network.webSocketFrameError', e => this._page._frameManager.webSocketError(e.requestId, e.errorMessage)), + this._session.addManagedListener('Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)), + this._session.addManagedListener('Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)), + this._session.addManagedListener('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId)), + this._session.addManagedListener('Page.frameDetached', event => this._onFrameDetached(event.frameId)), + this._session.addManagedListener('Page.willCheckNavigationPolicy', event => this._onWillCheckNavigationPolicy(event.frameId)), + this._session.addManagedListener('Page.didCheckNavigationPolicy', event => this._onDidCheckNavigationPolicy(event.frameId, event.cancel)), + this._session.addManagedListener('Page.frameScheduledNavigation', event => this._onFrameScheduledNavigation(event.frameId, event.delay, event.targetIsCurrentFrame)), + this._session.addManagedListener('Page.loadEventFired', event => this._page._frameManager.frameLifecycleEvent(event.frameId, 'load')), + this._session.addManagedListener('Page.domContentEventFired', event => this._page._frameManager.frameLifecycleEvent(event.frameId, 'domcontentloaded')), + this._session.addManagedListener('Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)), + this._session.addManagedListener('Runtime.bindingCalled', event => this._onBindingCalled(event.contextId, event.argument)), + this._session.addManagedListener('Console.messageAdded', event => this._onConsoleMessage(event)), + this._session.addManagedListener('Console.messageRepeatCountUpdated', event => this._onConsoleRepeatCountUpdated(event)), + this._pageProxySession.addManagedListener('Dialog.javascriptDialogOpening', event => this._onDialog(event)), + this._session.addManagedListener('Page.fileChooserOpened', event => this._onFileChooserOpened(event)), + this._session.addManagedListener('Network.requestWillBeSent', e => this._onRequestWillBeSent(this._session, e)), + this._session.addManagedListener('Network.requestIntercepted', e => this._onRequestIntercepted(this._session, e)), + this._session.addManagedListener('Network.responseReceived', e => this._onResponseReceived(this._session, e)), + this._session.addManagedListener('Network.loadingFinished', e => this._onLoadingFinished(e)), + this._session.addManagedListener('Network.loadingFailed', e => this._onLoadingFailed(this._session, e)), + this._session.addManagedListener('Network.webSocketCreated', e => this._page._frameManager.onWebSocketCreated(e.requestId, e.url)), + this._session.addManagedListener('Network.webSocketWillSendHandshakeRequest', e => this._page._frameManager.onWebSocketRequest(e.requestId)), + this._session.addManagedListener('Network.webSocketHandshakeResponseReceived', e => this._page._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)), + this._session.addManagedListener('Network.webSocketFrameSent', e => e.response.payloadData && this._page._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)), + this._session.addManagedListener('Network.webSocketFrameReceived', e => e.response.payloadData && this._page._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)), + this._session.addManagedListener('Network.webSocketClosed', e => this._page._frameManager.webSocketClosed(e.requestId)), + this._session.addManagedListener('Network.webSocketFrameError', e => this._page._frameManager.webSocketError(e.requestId, e.errorMessage)), ]; } private async _updateState( @@ -451,7 +450,7 @@ export class WKPage implements PageDelegate { } private _handleFrameTree(frameTree: Protocol.Page.FrameResourceTree) { - this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId || null); + this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId); this._onFrameNavigated(frameTree.frame, true); this._page._frameManager.frameLifecycleEvent(frameTree.frame.id, 'domcontentloaded'); this._page._frameManager.frameLifecycleEvent(frameTree.frame.id, 'load'); @@ -462,7 +461,7 @@ export class WKPage implements PageDelegate { this._handleFrameTree(child); } - _onFrameAttached(frameId: string, parentFrameId: string | null): frames.Frame { + _onFrameAttached(frameId: string, parentFrameId: string | undefined): frames.Frame { return this._page._frameManager.frameAttached(frameId, parentFrameId); } @@ -552,7 +551,7 @@ export class WKPage implements PageDelegate { error.stack = stack; error.name = name; - this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, error, this._page); + this._page.emitOnContextOnceInitialized('pageerror', error, this._page); return; } @@ -606,7 +605,7 @@ export class WKPage implements PageDelegate { } _onDialog(event: Protocol.Dialog.javascriptDialogOpeningPayload) { - this._page.emitOnContext(BrowserContext.Events.Dialog, new dialog.Dialog( + this._page.emitOnContext('dialog', new dialog.Dialog( this._page, event.type as dialog.DialogType, event.message, @@ -937,7 +936,7 @@ export class WKPage implements PageDelegate { this._pageProxySession.send('Screencast.screencastFrameAck', { generation }).catch(e => debugLogger.log('error', e)); }); const buffer = Buffer.from(event.data, 'base64'); - this._page.emit(Page.Events.ScreencastFrame, { + this._page.emit('screencastframe', { buffer, width: event.deviceWidth, height: event.deviceHeight, diff --git a/packages/playwright-core/src/server/webkit/wkProvisionalPage.ts b/packages/playwright-core/src/server/webkit/wkProvisionalPage.ts index 0b19ca7a17..a0ce678747 100644 --- a/packages/playwright-core/src/server/webkit/wkProvisionalPage.ts +++ b/packages/playwright-core/src/server/webkit/wkProvisionalPage.ts @@ -43,11 +43,11 @@ export class WKProvisionalPage { const wkPage = this._wkPage; this._sessionListeners = [ - eventsHelper.addEventListener(session, 'Network.requestWillBeSent', overrideFrameId(e => wkPage._onRequestWillBeSent(session, e))), - eventsHelper.addEventListener(session, 'Network.requestIntercepted', overrideFrameId(e => wkPage._onRequestIntercepted(session, e))), - eventsHelper.addEventListener(session, 'Network.responseReceived', overrideFrameId(e => wkPage._onResponseReceived(session, e))), - eventsHelper.addEventListener(session, 'Network.loadingFinished', overrideFrameId(e => wkPage._onLoadingFinished(e))), - eventsHelper.addEventListener(session, 'Network.loadingFailed', overrideFrameId(e => wkPage._onLoadingFailed(session, e))), + session.addManagedListener('Network.requestWillBeSent', overrideFrameId(e => wkPage._onRequestWillBeSent(session, e))), + session.addManagedListener('Network.requestIntercepted', overrideFrameId(e => wkPage._onRequestIntercepted(session, e))), + session.addManagedListener('Network.responseReceived', overrideFrameId(e => wkPage._onResponseReceived(session, e))), + session.addManagedListener('Network.loadingFinished', overrideFrameId(e => wkPage._onLoadingFinished(e))), + session.addManagedListener('Network.loadingFailed', overrideFrameId(e => wkPage._onLoadingFailed(session, e))), ]; this.initializationPromise = this._wkPage._initializeSession(session, true, ({ frameTree }) => this._handleFrameTree(frameTree)); @@ -59,7 +59,7 @@ export class WKProvisionalPage { commit() { assert(this._mainFrameId); - this._wkPage._onFrameAttached(this._mainFrameId, null); + this._wkPage._onFrameAttached(this._mainFrameId, undefined); } private _handleFrameTree(frameTree: Protocol.Page.FrameResourceTree) { diff --git a/packages/playwright-core/src/server/webkit/wkWorkers.ts b/packages/playwright-core/src/server/webkit/wkWorkers.ts index ae69b72f8a..6410daa5e4 100644 --- a/packages/playwright-core/src/server/webkit/wkWorkers.ts +++ b/packages/playwright-core/src/server/webkit/wkWorkers.ts @@ -36,7 +36,7 @@ export class WKWorkers { eventsHelper.removeEventListeners(this._sessionListeners); this.clear(); this._sessionListeners = [ - eventsHelper.addEventListener(session, 'Worker.workerCreated', (event: Protocol.Worker.workerCreatedPayload) => { + session.addManagedListener('Worker.workerCreated', (event: Protocol.Worker.workerCreatedPayload) => { const worker = new Worker(this._page, event.url); const workerSession = new WKSession(session.connection, event.workerId, (message: any) => { session.send('Worker.sendMessageToWorker', { @@ -59,13 +59,13 @@ export class WKWorkers { this._page._removeWorker(event.workerId); }); }), - eventsHelper.addEventListener(session, 'Worker.dispatchMessageFromWorker', (event: Protocol.Worker.dispatchMessageFromWorkerPayload) => { + session.addManagedListener('Worker.dispatchMessageFromWorker', (event: Protocol.Worker.dispatchMessageFromWorkerPayload) => { const workerSession = this._workerSessions.get(event.workerId)!; if (!workerSession) return; workerSession.dispatchMessage(JSON.parse(event.message)); }), - eventsHelper.addEventListener(session, 'Worker.workerTerminated', (event: Protocol.Worker.workerTerminatedPayload) => { + session.addManagedListener('Worker.workerTerminated', (event: Protocol.Worker.workerTerminatedPayload) => { const workerSession = this._workerSessions.get(event.workerId)!; if (!workerSession) return; diff --git a/packages/playwright-core/src/utils/eventsHelper.ts b/packages/playwright-core/src/utils/eventsHelper.ts index bdf9a1502f..0073c31677 100644 --- a/packages/playwright-core/src/utils/eventsHelper.ts +++ b/packages/playwright-core/src/utils/eventsHelper.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import type { EventEmitter } from 'events'; +import { EventEmitter } from 'events'; export type RegisteredListener = { emitter: EventEmitter; @@ -24,6 +24,8 @@ export type RegisteredListener = { }; class EventsHelper { + // Used for normal EventEmitter instances, in order to benefit from + // auto-completion, use ManagedEventEmitter.addManagedListener instead static addEventListener( emitter: EventEmitter, eventName: (string | symbol), @@ -43,4 +45,28 @@ class EventsHelper { } } +// From node_modules/@types/node/events.d.ts +type DefaultEventMap = [never]; +type Listener = T extends DefaultEventMap ? F : ( + K extends keyof T ? ( + T[K] extends unknown[] ? (...args: T[K]) => void : never + ) + : never +); +type Listener1 = Listener void>; +type Key = T extends DefaultEventMap ? string | symbol : K | keyof T; + +export class ManagedEventEmitter> extends EventEmitter { + public _events!: T; + + addManagedListener(eventName: Key, listener: Listener1): RegisteredListener { + this.on(eventName, listener); + return { + emitter: this as EventEmitter, + eventName: eventName as string, + handler: listener + }; + } +} + export const eventsHelper = EventsHelper; diff --git a/packages/playwright/src/runner/dispatcher.ts b/packages/playwright/src/runner/dispatcher.ts index ad44ce453e..100dc26253 100644 --- a/packages/playwright/src/runner/dispatcher.ts +++ b/packages/playwright/src/runner/dispatcher.ts @@ -504,13 +504,13 @@ class JobDispatcher { worker.runTestGroup(runPayload); this._listeners = [ - eventsHelper.addEventListener(worker, 'testBegin', this._onTestBegin.bind(this)), - eventsHelper.addEventListener(worker, 'testEnd', this._onTestEnd.bind(this)), - eventsHelper.addEventListener(worker, 'stepBegin', this._onStepBegin.bind(this)), - eventsHelper.addEventListener(worker, 'stepEnd', this._onStepEnd.bind(this)), - eventsHelper.addEventListener(worker, 'attach', this._onAttach.bind(this)), - eventsHelper.addEventListener(worker, 'done', this._onDone.bind(this)), - eventsHelper.addEventListener(worker, 'exit', this.onExit.bind(this)), + worker.addManagedListener('testBegin', this._onTestBegin.bind(this)), + worker.addManagedListener('testEnd', this._onTestEnd.bind(this)), + worker.addManagedListener('stepBegin', this._onStepBegin.bind(this)), + worker.addManagedListener('stepEnd', this._onStepEnd.bind(this)), + worker.addManagedListener('attach', this._onAttach.bind(this)), + worker.addManagedListener('done', this._onDone.bind(this)), + worker.addManagedListener('exit', this.onExit.bind(this)), ]; } diff --git a/packages/playwright/src/runner/processHost.ts b/packages/playwright/src/runner/processHost.ts index 4c77c17857..6b3a9a4242 100644 --- a/packages/playwright/src/runner/processHost.ts +++ b/packages/playwright/src/runner/processHost.ts @@ -15,13 +15,13 @@ */ import child_process from 'child_process'; -import { EventEmitter } from 'events'; import { debug } from 'playwright-core/lib/utilsBundle'; import type { EnvProducedPayload, ProcessInitParams } from '../common/ipc'; import type { ProtocolResponse } from '../common/process'; import { execArgvWithExperimentalLoaderOptions } from '../transform/esmUtils'; -import { assert } from 'playwright-core/lib/utils'; +import { assert, ManagedEventEmitter } from 'playwright-core/lib/utils'; import { esmLoaderRegistered } from '../common/esmLoaderHost'; +import type { WorkerHostEvents } from './workerHost'; export type ProcessExitData = { unexpectedly: boolean; @@ -29,7 +29,7 @@ export type ProcessExitData = { signal: NodeJS.Signals | null; }; -export class ProcessHost extends EventEmitter { +export class ProcessHost extends ManagedEventEmitter { private process: child_process.ChildProcess | undefined; private _didSendStop = false; private _processDidExit = false; @@ -69,7 +69,7 @@ export class ProcessHost extends EventEmitter { this._processDidExit = true; await this.onExit(); this._didExitAndRanOnExit = true; - this.emit('exit', { unexpectedly: !this._didSendStop, code, signal } as ProcessExitData); + this.emit('exit', { unexpectedly: !this._didSendStop, code, signal }); }); this.process.on('error', e => {}); // do not yell at a send to dead process. this.process.on('message', (message: any) => { @@ -91,6 +91,7 @@ export class ProcessHost extends EventEmitter { resolve(result); } } else { + // @ts-ignore this.emit(method!, params); } } else { @@ -148,7 +149,7 @@ export class ProcessHost extends EventEmitter { this._didSendStop = true; } if (!this._didExitAndRanOnExit) - await new Promise(f => this.once('exit', f)); + await new Promise(f => this.once('exit', f)); } didSendStop() { diff --git a/packages/playwright/src/runner/workerHost.ts b/packages/playwright/src/runner/workerHost.ts index c9029aaff0..eb0e00b70d 100644 --- a/packages/playwright/src/runner/workerHost.ts +++ b/packages/playwright/src/runner/workerHost.ts @@ -18,13 +18,29 @@ import fs from 'fs'; import path from 'path'; import type { TestGroup } from './testGroups'; import { stdioChunkToParams } from '../common/ipc'; -import type { RunPayload, SerializedConfig, WorkerInitParams } from '../common/ipc'; +import type { AttachmentPayload, DonePayload, RunPayload, SerializedConfig, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, TestBeginPayload, TestEndPayload, TestOutputPayload, WorkerInitParams } from '../common/ipc'; +import type { ProcessExitData } from './processHost'; import { ProcessHost } from './processHost'; import { artifactsFolderName } from '../isomorphic/folders'; import { removeFolders } from 'playwright-core/lib/utils'; let lastWorkerIndex = 0; +export type WorkerHostEvents = { + stdOut: [TestOutputPayload] + stdErr: [TestOutputPayload] + testBegin: [TestBeginPayload] + testEnd: [TestEndPayload] + stepBegin: [StepBeginPayload] + stepEnd: [StepEndPayload] + attach: [AttachmentPayload] + done: [DonePayload] + teardownErrors: [TeardownErrorsPayload] + + exit: [ProcessExitData] + ready: [] +}; + export class WorkerHost extends ProcessHost { readonly parallelIndex: number; readonly workerIndex: number; diff --git a/tests/config/serverFixtures.ts b/tests/config/serverFixtures.ts index a156f2ac5f..e4cc43b69b 100644 --- a/tests/config/serverFixtures.ts +++ b/tests/config/serverFixtures.ts @@ -18,9 +18,9 @@ import type { Fixtures } from '@playwright/test'; import path from 'path'; import { TestServer } from './testserver'; import { TestProxy } from './proxy'; -import type { SocksSocketRequestedPayload } from '../../packages/playwright-core/src/common/socksProxy'; -import { SocksProxy } from '../../packages/playwright-core/lib/common/socksProxy'; +import type { SocksProxy } from '../../packages/playwright-core/src/common/socksProxy'; +const SocksProxyImpl = require('../../packages/playwright-core/lib/common/socksProxy').SocksProxy as any as typeof SocksProxy; export type ServerWorkerOptions = { loopback?: string; @@ -100,16 +100,16 @@ export class MockSocksServer { private _socksProxy: SocksProxy; constructor() { - this._socksProxy = new SocksProxy(); + this._socksProxy = new SocksProxyImpl(); this._socksProxy.setPattern('*'); - this._socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => { + this._socksProxy.addListener('socksRequested', async payload => { this._socksProxy.socketConnected({ uid: payload.uid, host: '127.0.0.1', port: 0, }); }); - this._socksProxy.addListener(SocksProxy.Events.SocksData, async (payload: SocksSocketRequestedPayload) => { + this._socksProxy.addListener('socksData', async payload => { const body = 'Served by the SOCKS proxy'; const data = Buffer.from([ 'HTTP/1.1 200 OK', diff --git a/tests/library/proxy.spec.ts b/tests/library/proxy.spec.ts index 344763bb1d..10e3ba2ab8 100644 --- a/tests/library/proxy.spec.ts +++ b/tests/library/proxy.spec.ts @@ -17,7 +17,9 @@ import { playwrightTest as it, expect } from '../config/browserTest'; import net from 'net'; import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../../packages/playwright-core/src/common/socksProxy'; -import { SocksProxy } from '../../packages/playwright-core/lib/common/socksProxy'; + +import type { SocksProxy } from '../../packages/playwright-core/src/common/socksProxy'; +const SocksProxyImpl = require('../../packages/playwright-core/lib/common/socksProxy').SocksProxy as any as typeof SocksProxy; it.skip(({ mode }) => mode.startsWith('service')); @@ -291,9 +293,9 @@ it('should use proxy with emulated user agent', async ({ browserType }) => { async function setupSocksForwardingServer(port: number, forwardPort: number) { const connections = new Map(); - const socksProxy = new SocksProxy(); + const socksProxy = new SocksProxyImpl(); socksProxy.setPattern('*'); - socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => { + socksProxy.addListener('socksRequested', async (payload: SocksSocketRequestedPayload) => { if (!['127.0.0.1', 'fake-localhost-127-0-0-1.nip.io'].includes(payload.host) || payload.port !== 1337) { socksProxy.sendSocketError({ uid: payload.uid, error: 'ECONNREFUSED' }); return; @@ -309,10 +311,10 @@ async function setupSocksForwardingServer(port: number, forwardPort: number) { socksProxy.socketConnected({ uid: payload.uid, host: target.localAddress, port: target.localPort }); }); }); - socksProxy.addListener(SocksProxy.Events.SocksData, async (payload: SocksSocketDataPayload) => { + socksProxy.addListener('socksData', async (payload: SocksSocketDataPayload) => { connections.get(payload.uid)?.write(payload.data); }); - socksProxy.addListener(SocksProxy.Events.SocksClosed, (payload: SocksSocketClosedPayload) => { + socksProxy.addListener('socksClosed', (payload: SocksSocketClosedPayload) => { connections.get(payload.uid)?.destroy(); connections.delete(payload.uid); });