diff --git a/src/browserServerImpl.ts b/src/browserServerImpl.ts index 3097cf5deb..aefbe3da40 100644 --- a/src/browserServerImpl.ts +++ b/src/browserServerImpl.ts @@ -31,9 +31,9 @@ import { createGuid } from './utils/utils'; import { SelectorsDispatcher } from './dispatchers/selectorsDispatcher'; import { Selectors } from './server/selectors'; import { BrowserContext, Video } from './server/browserContext'; -import { StreamDispatcher, StreamWrapper } from './dispatchers/streamDispatcher'; +import { StreamDispatcher } from './dispatchers/streamDispatcher'; import { ProtocolLogger } from './server/types'; -import { SdkObject } from './server/sdkObject'; +import { CallMetadata, internalCallMetadata, SdkObject } from './server/instrumentation'; export class BrowserServerLauncherImpl implements BrowserServerLauncher { private _browserType: BrowserType; @@ -43,7 +43,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher { } async launchServer(options: LaunchServerOptions = {}): Promise { - const browser = await this._browserType.launch({ + const browser = await this._browserType.launch(internalCallMetadata(), { ...options, ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined, ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs), @@ -119,7 +119,7 @@ export class BrowserServerImpl extends EventEmitter implements BrowserServer { connection.dispatch(JSON.parse(Buffer.from(message).toString())); }); socket.on('error', () => {}); - const selectors = new Selectors(this._browser.options.rootSdkObject); + const selectors = new Selectors(); const scope = connection.rootDispatcher(); const remoteBrowser = new RemoteBrowserDispatcher(scope, this._browser, selectors); socket.on('close', () => { @@ -156,12 +156,12 @@ class ConnectedBrowser extends BrowserDispatcher { this._selectors = selectors; } - async newContext(params: channels.BrowserNewContextParams): Promise<{ context: channels.BrowserContextChannel }> { + async newContext(params: channels.BrowserNewContextParams, metadata: CallMetadata): Promise<{ context: channels.BrowserContextChannel }> { if (params.recordVideo) { // TODO: we should create a separate temp directory or accept a launchServer parameter. params.recordVideo.dir = this._object.options.downloadsPath!; } - const result = await super.newContext(params); + const result = await super.newContext(params, metadata); const dispatcher = result.context as BrowserContextDispatcher; dispatcher._object.on(BrowserContext.Events.VideoStarted, (video: Video) => this._sendVideo(dispatcher, video)); dispatcher._object._setSelectors(this._selectors); @@ -189,7 +189,7 @@ class ConnectedBrowser extends BrowserDispatcher { video._waitForCallbackOnFinish(async () => { const readable = fs.createReadStream(video._path); await new Promise(f => readable.on('readable', f)); - const stream = new StreamDispatcher(this._remoteBrowser!._scope, new StreamWrapper(this._object, readable)); + const stream = new StreamDispatcher(this._remoteBrowser!._scope, readable); this._remoteBrowser!._dispatchEvent('video', { stream, context: contextDispatcher, diff --git a/src/cli/traceViewer/screenshotGenerator.ts b/src/cli/traceViewer/screenshotGenerator.ts index ba4b8dc805..e5c1857cf3 100644 --- a/src/cli/traceViewer/screenshotGenerator.ts +++ b/src/cli/traceViewer/screenshotGenerator.ts @@ -74,11 +74,11 @@ export class ScreenshotGenerator { const snapshots = action.snapshots || []; const snapshotId = snapshots.length ? snapshots[0].snapshotId : undefined; const snapshotUrl = this._snapshotServer.snapshotUrl(action.pageId!, snapshotId, action.endTime); - console.log('Generating screenshot for ' + action.action); // eslint-disable-line no-console + console.log('Generating screenshot for ' + action.method); // eslint-disable-line no-console await page.evaluate(snapshotUrl => (window as any).showSnapshot(snapshotUrl), snapshotUrl); try { - const element = await page.$(action.selector || '*[__playwright_target__]'); + const element = await page.$(action.params.selector || '*[__playwright_target__]'); if (element) { await element.evaluate(e => { e.style.backgroundColor = '#ff69b460'; diff --git a/src/dispatchers/androidDispatcher.ts b/src/dispatchers/androidDispatcher.ts index 4345de462d..0d00f4a3c2 100644 --- a/src/dispatchers/androidDispatcher.ts +++ b/src/dispatchers/androidDispatcher.ts @@ -18,6 +18,7 @@ import { Dispatcher, DispatcherScope, existingDispatcher } from './dispatcher'; import { Android, AndroidDevice, SocketBackend } from '../server/android/android'; import * as channels from '../protocol/channels'; import { BrowserContextDispatcher } from './browserContextDispatcher'; +import { CallMetadata } from '../server/instrumentation'; export class AndroidDispatcher extends Dispatcher implements channels.AndroidChannel { constructor(scope: DispatcherScope, android: Android) { @@ -141,7 +142,7 @@ export class AndroidDeviceDispatcher extends Dispatcher { + async open(params: channels.AndroidDeviceOpenParams, metadata: CallMetadata): Promise { const socket = await this._object.open(params.command); return { socket: new AndroidSocketDispatcher(this._scope, socket) }; } @@ -182,11 +183,11 @@ export class AndroidSocketDispatcher extends Dispatcher { + async write(params: channels.AndroidSocketWriteParams, metadata: CallMetadata): Promise { await this._object.write(Buffer.from(params.data, 'base64')); } - async close(params: channels.AndroidSocketCloseParams, metadata?: channels.Metadata): Promise { + async close(params: channels.AndroidSocketCloseParams, metadata: CallMetadata): Promise { await this._object.close(); } } diff --git a/src/dispatchers/browserContextDispatcher.ts b/src/dispatchers/browserContextDispatcher.ts index 6f5a80ed34..51e75f5a5d 100644 --- a/src/dispatchers/browserContextDispatcher.ts +++ b/src/dispatchers/browserContextDispatcher.ts @@ -22,6 +22,7 @@ import { RouteDispatcher, RequestDispatcher } from './networkDispatchers'; import { CRBrowserContext } from '../server/chromium/crBrowser'; import { CDPSessionDispatcher } from './cdpSessionDispatcher'; import { RecorderSupplement } from '../server/supplements/recorderSupplement'; +import { CallMetadata } from '../server/instrumentation'; export class BrowserContextDispatcher extends Dispatcher implements channels.BrowserContextChannel { private _context: BrowserContext; @@ -120,8 +121,8 @@ export class BrowserContextDispatcher extends Dispatcher { - return await this._context.storageState(); + async storageState(params: channels.BrowserContextStorageStateParams, metadata: CallMetadata): Promise { + return await this._context.storageState(metadata); } async close(): Promise { diff --git a/src/dispatchers/browserDispatcher.ts b/src/dispatchers/browserDispatcher.ts index 66dd427352..5d711d6f50 100644 --- a/src/dispatchers/browserDispatcher.ts +++ b/src/dispatchers/browserDispatcher.ts @@ -21,6 +21,7 @@ import { CDPSessionDispatcher } from './cdpSessionDispatcher'; import { Dispatcher, DispatcherScope } from './dispatcher'; import { CRBrowser } from '../server/chromium/crBrowser'; import { PageDispatcher } from './pageDispatcher'; +import { CallMetadata } from '../server/instrumentation'; export class BrowserDispatcher extends Dispatcher implements channels.BrowserChannel { constructor(scope: DispatcherScope, browser: Browser) { @@ -33,10 +34,10 @@ export class BrowserDispatcher extends Dispatcher { + async newContext(params: channels.BrowserNewContextParams, metadata: CallMetadata): Promise { const context = await this._object.newContext(params); if (params.storageState) - await context.setStorageState(params.storageState); + await context.setStorageState(metadata, params.storageState); return { context: new BrowserContextDispatcher(this._scope, context) }; } diff --git a/src/dispatchers/browserTypeDispatcher.ts b/src/dispatchers/browserTypeDispatcher.ts index 39469762e9..ccf3bd14d6 100644 --- a/src/dispatchers/browserTypeDispatcher.ts +++ b/src/dispatchers/browserTypeDispatcher.ts @@ -19,6 +19,7 @@ import { BrowserDispatcher } from './browserDispatcher'; import * as channels from '../protocol/channels'; import { Dispatcher, DispatcherScope } from './dispatcher'; import { BrowserContextDispatcher } from './browserContextDispatcher'; +import { CallMetadata } from '../server/instrumentation'; export class BrowserTypeDispatcher extends Dispatcher implements channels.BrowserTypeChannel { constructor(scope: DispatcherScope, browserType: BrowserType) { @@ -28,13 +29,13 @@ export class BrowserTypeDispatcher extends Dispatcher { - const browser = await this._object.launch(params); + async launch(params: channels.BrowserTypeLaunchParams, metadata: CallMetadata): Promise { + const browser = await this._object.launch(metadata, params); return { browser: new BrowserDispatcher(this._scope, browser) }; } - async launchPersistentContext(params: channels.BrowserTypeLaunchPersistentContextParams): Promise { - const browserContext = await this._object.launchPersistentContext(params.userDataDir, params); + async launchPersistentContext(params: channels.BrowserTypeLaunchPersistentContextParams, metadata: CallMetadata): Promise { + const browserContext = await this._object.launchPersistentContext(metadata, params.userDataDir, params); return { context: new BrowserContextDispatcher(this._scope, browserContext) }; } } diff --git a/src/dispatchers/dispatcher.ts b/src/dispatchers/dispatcher.ts index 8b84fd3301..6f2c2941ee 100644 --- a/src/dispatchers/dispatcher.ts +++ b/src/dispatchers/dispatcher.ts @@ -21,7 +21,7 @@ import { createScheme, Validator, ValidationError } from '../protocol/validator' import { assert, createGuid, debugAssert, isUnderTest } from '../utils/utils'; import { tOptional } from '../protocol/validatorPrimitives'; import { kBrowserOrContextClosedError } from '../utils/errors'; -import { SdkObject } from '../server/sdkObject'; +import { CallMetadata } from '../server/instrumentation'; export const dispatcherSymbol = Symbol('dispatcher'); @@ -39,14 +39,7 @@ export function lookupNullableDispatcher(object: any | null): Di return object ? lookupDispatcher(object) : undefined; } -export type CallMetadata = channels.Metadata & { - object: SdkObject; - type: string; - method: string; - params: any; -}; - -export class Dispatcher extends EventEmitter implements channels.Channel { +export class Dispatcher extends EventEmitter implements channels.Channel { private _connection: DispatcherConnection; private _isScope: boolean; // Parent is always "isScope". @@ -120,9 +113,9 @@ export class Dispatcher extends EventEmitte } export type DispatcherScope = Dispatcher; -class Root extends Dispatcher { +class Root extends Dispatcher<{}, {}> { constructor(connection: DispatcherConnection) { - super(connection, new SdkObject(null), '', {}, true, ''); + super(connection, {}, '', {}, true, ''); } } @@ -186,8 +179,7 @@ export class DispatcherConnection { if (typeof (dispatcher as any)[method] !== 'function') throw new Error(`Mismatching dispatcher: "${dispatcher._type}" does not implement "${method}"`); const callMetadata: CallMetadata = { - ...this._validateMetadata(metadata).stack, - object: dispatcher._object, + ...this._validateMetadata(metadata), type: dispatcher._type, method, params, diff --git a/src/dispatchers/downloadDispatcher.ts b/src/dispatchers/downloadDispatcher.ts index 96ca7813ce..4e0e0d5e9f 100644 --- a/src/dispatchers/downloadDispatcher.ts +++ b/src/dispatchers/downloadDispatcher.ts @@ -17,7 +17,7 @@ import { Download } from '../server/download'; import * as channels from '../protocol/channels'; import { Dispatcher, DispatcherScope } from './dispatcher'; -import { StreamDispatcher, StreamWrapper } from './streamDispatcher'; +import { StreamDispatcher } from './streamDispatcher'; import * as fs from 'fs'; import * as util from 'util'; import { mkdirIfNeeded } from '../utils/utils'; @@ -65,7 +65,7 @@ export class DownloadDispatcher extends Dispatcher readable.on('readable', f)); - const stream = new StreamDispatcher(this._scope, new StreamWrapper(this._object, readable)); + const stream = new StreamDispatcher(this._scope, readable); // Resolve with a stream, so that client starts saving the data. resolve({ stream }); // Block the download until the stream is consumed. @@ -87,7 +87,7 @@ export class DownloadDispatcher extends Dispatcher readable.on('readable', f)); - return { stream: new StreamDispatcher(this._scope, new StreamWrapper(this._object, readable)) }; + return { stream: new StreamDispatcher(this._scope, readable) }; } async failure(): Promise { diff --git a/src/dispatchers/elementHandlerDispatcher.ts b/src/dispatchers/elementHandlerDispatcher.ts index 59a03504b1..ef52de8545 100644 --- a/src/dispatchers/elementHandlerDispatcher.ts +++ b/src/dispatchers/elementHandlerDispatcher.ts @@ -20,7 +20,7 @@ import * as channels from '../protocol/channels'; import { DispatcherScope, lookupNullableDispatcher } from './dispatcher'; import { JSHandleDispatcher, serializeResult, parseArgument } from './jsHandleDispatcher'; import { FrameDispatcher } from './frameDispatcher'; -import { runAction } from '../server/browserContext'; +import { CallMetadata } from '../server/instrumentation'; export function createHandle(scope: DispatcherScope, handle: js.JSHandle): JSHandleDispatcher { return handle.asElement() ? new ElementHandleDispatcher(scope, handle.asElement()!) : new JSHandleDispatcher(scope, handle); @@ -40,171 +40,149 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann this._elementHandle = elementHandle; } - async ownerFrame(): Promise { + async ownerFrame(params: channels.ElementHandleOwnerFrameParams, metadata: CallMetadata): Promise { return { frame: lookupNullableDispatcher(await this._elementHandle.ownerFrame()) }; } - async contentFrame(): Promise { + async contentFrame(params: channels.ElementHandleContentFrameParams, metadata: CallMetadata): Promise { return { frame: lookupNullableDispatcher(await this._elementHandle.contentFrame()) }; } - async getAttribute(params: channels.ElementHandleGetAttributeParams): Promise { + async getAttribute(params: channels.ElementHandleGetAttributeParams, metadata: CallMetadata): Promise { const value = await this._elementHandle.getAttribute(params.name); return { value: value === null ? undefined : value }; } - async textContent(): Promise { + async textContent(params: channels.ElementHandleTextContentParams, metadata: CallMetadata): Promise { const value = await this._elementHandle.textContent(); return { value: value === null ? undefined : value }; } - async innerText(): Promise { + async innerText(params: channels.ElementHandleInnerTextParams, metadata: CallMetadata): Promise { return { value: await this._elementHandle.innerText() }; } - async innerHTML(): Promise { + async innerHTML(params: channels.ElementHandleInnerHTMLParams, metadata: CallMetadata): Promise { return { value: await this._elementHandle.innerHTML() }; } - async isChecked(): Promise { + async isChecked(params: channels.ElementHandleIsCheckedParams, metadata: CallMetadata): Promise { return { value: await this._elementHandle.isChecked() }; } - async isDisabled(): Promise { + async isDisabled(params: channels.ElementHandleIsDisabledParams, metadata: CallMetadata): Promise { return { value: await this._elementHandle.isDisabled() }; } - async isEditable(): Promise { + async isEditable(params: channels.ElementHandleIsEditableParams, metadata: CallMetadata): Promise { return { value: await this._elementHandle.isEditable() }; } - async isEnabled(): Promise { + async isEnabled(params: channels.ElementHandleIsEnabledParams, metadata: CallMetadata): Promise { return { value: await this._elementHandle.isEnabled() }; } - async isHidden(): Promise { + async isHidden(params: channels.ElementHandleIsHiddenParams, metadata: CallMetadata): Promise { return { value: await this._elementHandle.isHidden() }; } - async isVisible(): Promise { + async isVisible(params: channels.ElementHandleIsVisibleParams, metadata: CallMetadata): Promise { return { value: await this._elementHandle.isVisible() }; } - async dispatchEvent(params: channels.ElementHandleDispatchEventParams): Promise { + async dispatchEvent(params: channels.ElementHandleDispatchEventParams, metadata: CallMetadata): Promise { await this._elementHandle.dispatchEvent(params.type, parseArgument(params.eventInit)); } - async scrollIntoViewIfNeeded(params: channels.ElementHandleScrollIntoViewIfNeededParams): Promise { - await this._elementHandle.scrollIntoViewIfNeeded(params); + async scrollIntoViewIfNeeded(params: channels.ElementHandleScrollIntoViewIfNeededParams, metadata: CallMetadata): Promise { + await this._elementHandle.scrollIntoViewIfNeeded(metadata, params); } - async hover(params: channels.ElementHandleHoverParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._elementHandle.hover(controller, params); - }, { ...metadata, type: 'hover', target: this._elementHandle, page: this._elementHandle._page }); + async hover(params: channels.ElementHandleHoverParams, metadata: CallMetadata): Promise { + return await this._elementHandle.hover(metadata, params); } - async click(params: channels.ElementHandleClickParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._elementHandle.click(controller, params); - }, { ...metadata, type: 'click', target: this._elementHandle, page: this._elementHandle._page }); + async click(params: channels.ElementHandleClickParams, metadata: CallMetadata): Promise { + return await this._elementHandle.click(metadata, params); } - async dblclick(params: channels.ElementHandleDblclickParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._elementHandle.dblclick(controller, params); - }, { ...metadata, type: 'dblclick', target: this._elementHandle, page: this._elementHandle._page }); + async dblclick(params: channels.ElementHandleDblclickParams, metadata: CallMetadata): Promise { + return await this._elementHandle.dblclick(metadata, params); } - async tap(params: channels.ElementHandleTapParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._elementHandle.tap(controller, params); - }, { ...metadata, type: 'tap', target: this._elementHandle, page: this._elementHandle._page }); + async tap(params: channels.ElementHandleTapParams, metadata: CallMetadata): Promise { + return await this._elementHandle.tap(metadata, params); } - async selectOption(params: channels.ElementHandleSelectOptionParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - const elements = (params.elements || []).map(e => (e as ElementHandleDispatcher)._elementHandle); - return { values: await this._elementHandle.selectOption(controller, elements, params.options || [], params) }; - }, { ...metadata, type: 'selectOption', target: this._elementHandle, page: this._elementHandle._page }); + async selectOption(params: channels.ElementHandleSelectOptionParams, metadata: CallMetadata): Promise { + const elements = (params.elements || []).map(e => (e as ElementHandleDispatcher)._elementHandle); + return { values: await this._elementHandle.selectOption(metadata, elements, params.options || [], params) }; } - async fill(params: channels.ElementHandleFillParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._elementHandle.fill(controller, params.value, params); - }, { ...metadata, type: 'fill', value: params.value, target: this._elementHandle, page: this._elementHandle._page }); + async fill(params: channels.ElementHandleFillParams, metadata: CallMetadata): Promise { + return await this._elementHandle.fill(metadata, params.value, params); } - async selectText(params: channels.ElementHandleSelectTextParams): Promise { - await this._elementHandle.selectText(params); + async selectText(params: channels.ElementHandleSelectTextParams, metadata: CallMetadata): Promise { + await this._elementHandle.selectText(metadata, params); } - async setInputFiles(params: channels.ElementHandleSetInputFilesParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._elementHandle.setInputFiles(controller, params.files, params); - }, { ...metadata, type: 'setInputFiles', target: this._elementHandle, page: this._elementHandle._page }); + async setInputFiles(params: channels.ElementHandleSetInputFilesParams, metadata: CallMetadata): Promise { + return await this._elementHandle.setInputFiles(metadata, params.files, params); } - async focus(): Promise { - await this._elementHandle.focus(); + async focus(params: channels.ElementHandleFocusParams, metadata: CallMetadata): Promise { + await this._elementHandle.focus(metadata); } - async type(params: channels.ElementHandleTypeParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._elementHandle.type(controller, params.text, params); - }, { ...metadata, type: 'type', value: params.text, target: this._elementHandle, page: this._elementHandle._page }); + async type(params: channels.ElementHandleTypeParams, metadata: CallMetadata): Promise { + return await this._elementHandle.type(metadata, params.text, params); } - async press(params: channels.ElementHandlePressParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._elementHandle.press(controller, params.key, params); - }, { ...metadata, type: 'press', value: params.key, target: this._elementHandle, page: this._elementHandle._page }); + async press(params: channels.ElementHandlePressParams, metadata: CallMetadata): Promise { + return await this._elementHandle.press(metadata, params.key, params); } - async check(params: channels.ElementHandleCheckParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._elementHandle.check(controller, params); - }, { ...metadata, type: 'check', target: this._elementHandle, page: this._elementHandle._page }); + async check(params: channels.ElementHandleCheckParams, metadata: CallMetadata): Promise { + return await this._elementHandle.check(metadata, params); } - async uncheck(params: channels.ElementHandleUncheckParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._elementHandle.uncheck(controller, params); - }, { ...metadata, type: 'uncheck', target: this._elementHandle, page: this._elementHandle._page }); + async uncheck(params: channels.ElementHandleUncheckParams, metadata: CallMetadata): Promise { + return await this._elementHandle.uncheck(metadata, params); } - async boundingBox(): Promise { + async boundingBox(params: channels.ElementHandleBoundingBoxParams, metadata: CallMetadata): Promise { const value = await this._elementHandle.boundingBox(); return { value: value || undefined }; } - async screenshot(params: channels.ElementHandleScreenshotParams): Promise { - return { binary: (await this._elementHandle.screenshot(params)).toString('base64') }; + async screenshot(params: channels.ElementHandleScreenshotParams, metadata: CallMetadata): Promise { + return { binary: (await this._elementHandle.screenshot(metadata, params)).toString('base64') }; } - async querySelector(params: channels.ElementHandleQuerySelectorParams): Promise { + async querySelector(params: channels.ElementHandleQuerySelectorParams, metadata: CallMetadata): Promise { const handle = await this._elementHandle.$(params.selector); return { element: handle ? new ElementHandleDispatcher(this._scope, handle) : undefined }; } - async querySelectorAll(params: channels.ElementHandleQuerySelectorAllParams): Promise { + async querySelectorAll(params: channels.ElementHandleQuerySelectorAllParams, metadata: CallMetadata): Promise { const elements = await this._elementHandle.$$(params.selector); return { elements: elements.map(e => new ElementHandleDispatcher(this._scope, e)) }; } - async evalOnSelector(params: channels.ElementHandleEvalOnSelectorParams): Promise { + async evalOnSelector(params: channels.ElementHandleEvalOnSelectorParams, metadata: CallMetadata): Promise { return { value: serializeResult(await this._elementHandle._$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; } - async evalOnSelectorAll(params: channels.ElementHandleEvalOnSelectorAllParams): Promise { + async evalOnSelectorAll(params: channels.ElementHandleEvalOnSelectorAllParams, metadata: CallMetadata): Promise { return { value: serializeResult(await this._elementHandle._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; } - async waitForElementState(params: channels.ElementHandleWaitForElementStateParams): Promise { - await this._elementHandle.waitForElementState(params.state, params); + async waitForElementState(params: channels.ElementHandleWaitForElementStateParams, metadata: CallMetadata): Promise { + await this._elementHandle.waitForElementState(metadata, params.state, params); } - async waitForSelector(params: channels.ElementHandleWaitForSelectorParams): Promise { - return { element: ElementHandleDispatcher.createNullable(this._scope, await this._elementHandle.waitForSelector(params.selector, params)) }; + async waitForSelector(params: channels.ElementHandleWaitForSelectorParams, metadata: CallMetadata): Promise { + return { element: ElementHandleDispatcher.createNullable(this._scope, await this._elementHandle.waitForSelector(metadata, params.selector, params)) }; } } diff --git a/src/dispatchers/frameDispatcher.ts b/src/dispatchers/frameDispatcher.ts index 226a16990b..08482612a9 100644 --- a/src/dispatchers/frameDispatcher.ts +++ b/src/dispatchers/frameDispatcher.ts @@ -20,7 +20,7 @@ import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatch import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher'; import { parseArgument, serializeResult } from './jsHandleDispatcher'; import { ResponseDispatcher, RequestDispatcher } from './networkDispatchers'; -import { runAction } from '../server/browserContext'; +import { CallMetadata } from '../server/instrumentation'; export class FrameDispatcher extends Dispatcher implements channels.FrameChannel { private _frame: Frame; @@ -52,45 +52,43 @@ export class FrameDispatcher extends Dispatcher { - return await runAction(async controller => { - return { response: lookupNullableDispatcher(await this._frame.goto(controller, params.url, params)) }; - }, { ...metadata, type: 'goto', value: params.url, page: this._frame._page }); + async goto(params: channels.FrameGotoParams, metadata: CallMetadata): Promise { + return { response: lookupNullableDispatcher(await this._frame.goto(metadata, params.url, params)) }; } async frameElement(): Promise { return { element: new ElementHandleDispatcher(this._scope, await this._frame.frameElement()) }; } - async evaluateExpression(params: channels.FrameEvaluateExpressionParams): Promise { + async evaluateExpression(params: channels.FrameEvaluateExpressionParams, metadata: CallMetadata): Promise { return { value: serializeResult(await this._frame._evaluateExpression(params.expression, params.isFunction, parseArgument(params.arg), params.world)) }; } - async evaluateExpressionHandle(params: channels.FrameEvaluateExpressionHandleParams): Promise { + async evaluateExpressionHandle(params: channels.FrameEvaluateExpressionHandleParams, metadata: CallMetadata): Promise { return { handle: createHandle(this._scope, await this._frame._evaluateExpressionHandle(params.expression, params.isFunction, parseArgument(params.arg), params.world)) }; } - async waitForSelector(params: channels.FrameWaitForSelectorParams): Promise { - return { element: ElementHandleDispatcher.createNullable(this._scope, await this._frame.waitForSelector(params.selector, params)) }; + async waitForSelector(params: channels.FrameWaitForSelectorParams, metadata: CallMetadata): Promise { + return { element: ElementHandleDispatcher.createNullable(this._scope, await this._frame.waitForSelector(metadata, params.selector, params)) }; } - async dispatchEvent(params: channels.FrameDispatchEventParams): Promise { - return this._frame.dispatchEvent(params.selector, params.type, parseArgument(params.eventInit), params); + async dispatchEvent(params: channels.FrameDispatchEventParams, metadata: CallMetadata): Promise { + return this._frame.dispatchEvent(metadata, params.selector, params.type, parseArgument(params.eventInit), params); } - async evalOnSelector(params: channels.FrameEvalOnSelectorParams): Promise { + async evalOnSelector(params: channels.FrameEvalOnSelectorParams, metadata: CallMetadata): Promise { return { value: serializeResult(await this._frame._$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; } - async evalOnSelectorAll(params: channels.FrameEvalOnSelectorAllParams): Promise { + async evalOnSelectorAll(params: channels.FrameEvalOnSelectorAllParams, metadata: CallMetadata): Promise { return { value: serializeResult(await this._frame._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; } - async querySelector(params: channels.FrameQuerySelectorParams): Promise { + async querySelector(params: channels.FrameQuerySelectorParams, metadata: CallMetadata): Promise { return { element: ElementHandleDispatcher.createNullable(this._scope, await this._frame.$(params.selector)) }; } - async querySelectorAll(params: channels.FrameQuerySelectorAllParams): Promise { + async querySelectorAll(params: channels.FrameQuerySelectorAllParams, metadata: CallMetadata): Promise { const elements = await this._frame.$$(params.selector); return { elements: elements.map(e => new ElementHandleDispatcher(this._scope, e)) }; } @@ -99,138 +97,114 @@ export class FrameDispatcher extends Dispatcher { - return await runAction(async controller => { - return await this._frame.setContent(controller, params.html, params); - }, { ...metadata, type: 'setContent', value: params.html, page: this._frame._page }); + async setContent(params: channels.FrameSetContentParams, metadata: CallMetadata): Promise { + return await this._frame.setContent(metadata, params.html, params); } - async addScriptTag(params: channels.FrameAddScriptTagParams): Promise { + async addScriptTag(params: channels.FrameAddScriptTagParams, metadata: CallMetadata): Promise { return { element: new ElementHandleDispatcher(this._scope, await this._frame.addScriptTag(params)) }; } - async addStyleTag(params: channels.FrameAddStyleTagParams): Promise { + async addStyleTag(params: channels.FrameAddStyleTagParams, metadata: CallMetadata): Promise { return { element: new ElementHandleDispatcher(this._scope, await this._frame.addStyleTag(params)) }; } - async click(params: channels.FrameClickParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._frame.click(controller, params.selector, params); - }, { ...metadata, type: 'click', target: params.selector, page: this._frame._page }); + async click(params: channels.FrameClickParams, metadata: CallMetadata): Promise { + return await this._frame.click(metadata, params.selector, params); } - async dblclick(params: channels.FrameDblclickParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._frame.dblclick(controller, params.selector, params); - }, { ...metadata, type: 'dblclick', target: params.selector, page: this._frame._page }); + async dblclick(params: channels.FrameDblclickParams, metadata: CallMetadata): Promise { + return await this._frame.dblclick(metadata, params.selector, params); } - async tap(params: channels.FrameTapParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._frame.tap(controller, params.selector, params); - }, { ...metadata, type: 'tap', target: params.selector, page: this._frame._page }); + async tap(params: channels.FrameTapParams, metadata: CallMetadata): Promise { + return await this._frame.tap(metadata, params.selector, params); } - async fill(params: channels.FrameFillParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._frame.fill(controller, params.selector, params.value, params); - }, { ...metadata, type: 'fill', value: params.value, target: params.selector, page: this._frame._page }); + async fill(params: channels.FrameFillParams, metadata: CallMetadata): Promise { + return await this._frame.fill(metadata, params.selector, params.value, params); } - async focus(params: channels.FrameFocusParams): Promise { - await this._frame.focus(params.selector, params); + async focus(params: channels.FrameFocusParams, metadata: CallMetadata): Promise { + await this._frame.focus(metadata, params.selector, params); } - async textContent(params: channels.FrameTextContentParams): Promise { - const value = await this._frame.textContent(params.selector, params); + async textContent(params: channels.FrameTextContentParams, metadata: CallMetadata): Promise { + const value = await this._frame.textContent(metadata, params.selector, params); return { value: value === null ? undefined : value }; } - async innerText(params: channels.FrameInnerTextParams): Promise { - return { value: await this._frame.innerText(params.selector, params) }; + async innerText(params: channels.FrameInnerTextParams, metadata: CallMetadata): Promise { + return { value: await this._frame.innerText(metadata, params.selector, params) }; } - async innerHTML(params: channels.FrameInnerHTMLParams): Promise { - return { value: await this._frame.innerHTML(params.selector, params) }; + async innerHTML(params: channels.FrameInnerHTMLParams, metadata: CallMetadata): Promise { + return { value: await this._frame.innerHTML(metadata, params.selector, params) }; } - async getAttribute(params: channels.FrameGetAttributeParams): Promise { - const value = await this._frame.getAttribute(params.selector, params.name, params); + async getAttribute(params: channels.FrameGetAttributeParams, metadata: CallMetadata): Promise { + const value = await this._frame.getAttribute(metadata, params.selector, params.name, params); return { value: value === null ? undefined : value }; } - async isChecked(params: channels.FrameIsCheckedParams): Promise { - return { value: await this._frame.isChecked(params.selector, params) }; + async isChecked(params: channels.FrameIsCheckedParams, metadata: CallMetadata): Promise { + return { value: await this._frame.isChecked(metadata, params.selector, params) }; } - async isDisabled(params: channels.FrameIsDisabledParams): Promise { - return { value: await this._frame.isDisabled(params.selector, params) }; + async isDisabled(params: channels.FrameIsDisabledParams, metadata: CallMetadata): Promise { + return { value: await this._frame.isDisabled(metadata, params.selector, params) }; } - async isEditable(params: channels.FrameIsEditableParams): Promise { - return { value: await this._frame.isEditable(params.selector, params) }; + async isEditable(params: channels.FrameIsEditableParams, metadata: CallMetadata): Promise { + return { value: await this._frame.isEditable(metadata, params.selector, params) }; } - async isEnabled(params: channels.FrameIsEnabledParams): Promise { - return { value: await this._frame.isEnabled(params.selector, params) }; + async isEnabled(params: channels.FrameIsEnabledParams, metadata: CallMetadata): Promise { + return { value: await this._frame.isEnabled(metadata, params.selector, params) }; } - async isHidden(params: channels.FrameIsHiddenParams): Promise { - return { value: await this._frame.isHidden(params.selector, params) }; + async isHidden(params: channels.FrameIsHiddenParams, metadata: CallMetadata): Promise { + return { value: await this._frame.isHidden(metadata, params.selector, params) }; } - async isVisible(params: channels.FrameIsVisibleParams): Promise { - return { value: await this._frame.isVisible(params.selector, params) }; + async isVisible(params: channels.FrameIsVisibleParams, metadata: CallMetadata): Promise { + return { value: await this._frame.isVisible(metadata, params.selector, params) }; } - async hover(params: channels.FrameHoverParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._frame.hover(controller, params.selector, params); - }, { ...metadata, type: 'hover', target: params.selector, page: this._frame._page }); + async hover(params: channels.FrameHoverParams, metadata: CallMetadata): Promise { + return await this._frame.hover(metadata, params.selector, params); } - async selectOption(params: channels.FrameSelectOptionParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - const elements = (params.elements || []).map(e => (e as ElementHandleDispatcher)._elementHandle); - return { values: await this._frame.selectOption(controller, params.selector, elements, params.options || [], params) }; - }, { ...metadata, type: 'selectOption', target: params.selector, page: this._frame._page }); + async selectOption(params: channels.FrameSelectOptionParams, metadata: CallMetadata): Promise { + const elements = (params.elements || []).map(e => (e as ElementHandleDispatcher)._elementHandle); + return { values: await this._frame.selectOption(metadata, params.selector, elements, params.options || [], params) }; } - async setInputFiles(params: channels.FrameSetInputFilesParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._frame.setInputFiles(controller, params.selector, params.files, params); - }, { ...metadata, type: 'setInputFiles', target: params.selector, page: this._frame._page }); + async setInputFiles(params: channels.FrameSetInputFilesParams, metadata: CallMetadata): Promise { + return await this._frame.setInputFiles(metadata, params.selector, params.files, params); } - async type(params: channels.FrameTypeParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._frame.type(controller, params.selector, params.text, params); - }, { ...metadata, type: 'type', value: params.text, target: params.selector, page: this._frame._page }); + async type(params: channels.FrameTypeParams, metadata: CallMetadata): Promise { + return await this._frame.type(metadata, params.selector, params.text, params); } - async press(params: channels.FramePressParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._frame.press(controller, params.selector, params.key, params); - }, { ...metadata, type: 'press', value: params.key, target: params.selector, page: this._frame._page }); + async press(params: channels.FramePressParams, metadata: CallMetadata): Promise { + return await this._frame.press(metadata, params.selector, params.key, params); } - async check(params: channels.FrameCheckParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._frame.check(controller, params.selector, params); - }, { ...metadata, type: 'check', target: params.selector, page: this._frame._page }); + async check(params: channels.FrameCheckParams, metadata: CallMetadata): Promise { + return await this._frame.check(metadata, params.selector, params); } - async uncheck(params: channels.FrameUncheckParams, metadata?: channels.Metadata): Promise { - return runAction(async controller => { - return await this._frame.uncheck(controller, params.selector, params); - }, { ...metadata, type: 'uncheck', target: params.selector, page: this._frame._page }); + async uncheck(params: channels.FrameUncheckParams, metadata: CallMetadata): Promise { + return await this._frame.uncheck(metadata, params.selector, params); } - async waitForFunction(params: channels.FrameWaitForFunctionParams): Promise { - return { handle: createHandle(this._scope, await this._frame._waitForFunctionExpression(params.expression, params.isFunction, parseArgument(params.arg), params)) }; + async waitForFunction(params: channels.FrameWaitForFunctionParams, metadata: CallMetadata): Promise { + return { handle: createHandle(this._scope, await this._frame._waitForFunctionExpression(metadata, params.expression, params.isFunction, parseArgument(params.arg), params)) }; } - async title(): Promise { + async title(params: channels.FrameTitleParams, metadata: CallMetadata): Promise { return { value: await this._frame.title() }; } } diff --git a/src/dispatchers/pageDispatcher.ts b/src/dispatchers/pageDispatcher.ts index 99386eb105..19940c5c6e 100644 --- a/src/dispatchers/pageDispatcher.ts +++ b/src/dispatchers/pageDispatcher.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { BrowserContext, runAction, Video } from '../server/browserContext'; +import { BrowserContext, Video } from '../server/browserContext'; import { Frame } from '../server/frames'; import { Request } from '../server/network'; import { Page, Worker } from '../server/page'; @@ -31,7 +31,7 @@ import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatche import { FileChooser } from '../server/fileChooser'; import { CRCoverage } from '../server/chromium/crCoverage'; import { JSHandle } from '../server/javascript'; -import { SdkObject } from '../server/sdkObject'; +import { CallMetadata } from '../server/instrumentation'; export class PageDispatcher extends Dispatcher implements channels.PageChannel { private _page: Page; @@ -80,19 +80,19 @@ export class PageDispatcher extends Dispatcher i page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) })); } - async setDefaultNavigationTimeoutNoReply(params: channels.PageSetDefaultNavigationTimeoutNoReplyParams): Promise { + async setDefaultNavigationTimeoutNoReply(params: channels.PageSetDefaultNavigationTimeoutNoReplyParams, metadata: CallMetadata): Promise { this._page.setDefaultNavigationTimeout(params.timeout); } - async setDefaultTimeoutNoReply(params: channels.PageSetDefaultTimeoutNoReplyParams): Promise { + async setDefaultTimeoutNoReply(params: channels.PageSetDefaultTimeoutNoReplyParams, metadata: CallMetadata): Promise { this._page.setDefaultTimeout(params.timeout); } - async opener(): Promise { + async opener(params: channels.PageOpenerParams, metadata: CallMetadata): Promise { return { page: lookupNullableDispatcher(await this._page.opener()) }; } - async exposeBinding(params: channels.PageExposeBindingParams): Promise { + async exposeBinding(params: channels.PageExposeBindingParams, metadata: CallMetadata): Promise { await this._page.exposeBinding(params.name, !!params.needsHandle, (source, ...args) => { const binding = new BindingCallDispatcher(this._scope, params.name, !!params.needsHandle, source, args); this._dispatchEvent('bindingCall', { binding }); @@ -100,44 +100,38 @@ export class PageDispatcher extends Dispatcher i }); } - async setExtraHTTPHeaders(params: channels.PageSetExtraHTTPHeadersParams): Promise { + async setExtraHTTPHeaders(params: channels.PageSetExtraHTTPHeadersParams, metadata: CallMetadata): Promise { await this._page.setExtraHTTPHeaders(params.headers); } - async reload(params: channels.PageReloadParams, metadata?: channels.Metadata): Promise { - return await runAction(async controller => { - return { response: lookupNullableDispatcher(await this._page.reload(controller, params)) }; - }, { ...metadata, type: 'reload', page: this._page }); + async reload(params: channels.PageReloadParams, metadata: CallMetadata): Promise { + return { response: lookupNullableDispatcher(await this._page.reload(metadata, params)) }; } - async goBack(params: channels.PageGoBackParams, metadata?: channels.Metadata): Promise { - return await runAction(async controller => { - return { response: lookupNullableDispatcher(await this._page.goBack(controller, params)) }; - }, { ...metadata, type: 'goBack', page: this._page }); + async goBack(params: channels.PageGoBackParams, metadata: CallMetadata): Promise { + return { response: lookupNullableDispatcher(await this._page.goBack(metadata, params)) }; } - async goForward(params: channels.PageGoForwardParams, metadata?: channels.Metadata): Promise { - return await runAction(async controller => { - return { response: lookupNullableDispatcher(await this._page.goForward(controller, params)) }; - }, { ...metadata, type: 'goForward', page: this._page }); + async goForward(params: channels.PageGoForwardParams, metadata: CallMetadata): Promise { + return { response: lookupNullableDispatcher(await this._page.goForward(metadata, params)) }; } - async emulateMedia(params: channels.PageEmulateMediaParams): Promise { + async emulateMedia(params: channels.PageEmulateMediaParams, metadata: CallMetadata): Promise { await this._page.emulateMedia({ media: params.media === 'null' ? null : params.media, colorScheme: params.colorScheme === 'null' ? null : params.colorScheme, }); } - async setViewportSize(params: channels.PageSetViewportSizeParams): Promise { + async setViewportSize(params: channels.PageSetViewportSizeParams, metadata: CallMetadata): Promise { await this._page.setViewportSize(params.viewportSize); } - async addInitScript(params: channels.PageAddInitScriptParams): Promise { + async addInitScript(params: channels.PageAddInitScriptParams, metadata: CallMetadata): Promise { await this._page._addInitScriptExpression(params.source); } - async setNetworkInterceptionEnabled(params: channels.PageSetNetworkInterceptionEnabledParams): Promise { + async setNetworkInterceptionEnabled(params: channels.PageSetNetworkInterceptionEnabledParams, metadata: CallMetadata): Promise { if (!params.enabled) { await this._page._setClientRequestInterceptor(undefined); return; @@ -147,59 +141,59 @@ export class PageDispatcher extends Dispatcher i }); } - async screenshot(params: channels.PageScreenshotParams): Promise { - return { binary: (await this._page.screenshot(params)).toString('base64') }; + async screenshot(params: channels.PageScreenshotParams, metadata: CallMetadata): Promise { + return { binary: (await this._page.screenshot(metadata, params)).toString('base64') }; } - async close(params: channels.PageCloseParams): Promise { + async close(params: channels.PageCloseParams, metadata: CallMetadata): Promise { await this._page.close(params); } - async setFileChooserInterceptedNoReply(params: channels.PageSetFileChooserInterceptedNoReplyParams): Promise { + async setFileChooserInterceptedNoReply(params: channels.PageSetFileChooserInterceptedNoReplyParams, metadata: CallMetadata): Promise { await this._page._setFileChooserIntercepted(params.intercepted); } - async keyboardDown(params: channels.PageKeyboardDownParams): Promise { + async keyboardDown(params: channels.PageKeyboardDownParams, metadata: CallMetadata): Promise { await this._page.keyboard.down(params.key); } - async keyboardUp(params: channels.PageKeyboardUpParams): Promise { + async keyboardUp(params: channels.PageKeyboardUpParams, metadata: CallMetadata): Promise { await this._page.keyboard.up(params.key); } - async keyboardInsertText(params: channels.PageKeyboardInsertTextParams): Promise { + async keyboardInsertText(params: channels.PageKeyboardInsertTextParams, metadata: CallMetadata): Promise { await this._page.keyboard.insertText(params.text); } - async keyboardType(params: channels.PageKeyboardTypeParams): Promise { + async keyboardType(params: channels.PageKeyboardTypeParams, metadata: CallMetadata): Promise { await this._page.keyboard.type(params.text, params); } - async keyboardPress(params: channels.PageKeyboardPressParams): Promise { + async keyboardPress(params: channels.PageKeyboardPressParams, metadata: CallMetadata): Promise { await this._page.keyboard.press(params.key, params); } - async mouseMove(params: channels.PageMouseMoveParams): Promise { + async mouseMove(params: channels.PageMouseMoveParams, metadata: CallMetadata): Promise { await this._page.mouse.move(params.x, params.y, params); } - async mouseDown(params: channels.PageMouseDownParams): Promise { + async mouseDown(params: channels.PageMouseDownParams, metadata: CallMetadata): Promise { await this._page.mouse.down(params); } - async mouseUp(params: channels.PageMouseUpParams): Promise { + async mouseUp(params: channels.PageMouseUpParams, metadata: CallMetadata): Promise { await this._page.mouse.up(params); } - async mouseClick(params: channels.PageMouseClickParams): Promise { + async mouseClick(params: channels.PageMouseClickParams, metadata: CallMetadata): Promise { await this._page.mouse.click(params.x, params.y, params); } - async touchscreenTap(params: channels.PageTouchscreenTapParams): Promise { + async touchscreenTap(params: channels.PageTouchscreenTapParams, metadata: CallMetadata): Promise { await this._page.touchscreen.tap(params.x, params.y); } - async accessibilitySnapshot(params: channels.PageAccessibilitySnapshotParams): Promise { + async accessibilitySnapshot(params: channels.PageAccessibilitySnapshotParams, metadata: CallMetadata): Promise { const rootAXNode = await this._page.accessibility.snapshot({ interestingOnly: params.interestingOnly, root: params.root ? (params.root as ElementHandleDispatcher)._elementHandle : undefined @@ -207,33 +201,33 @@ export class PageDispatcher extends Dispatcher i return { rootAXNode: rootAXNode || undefined }; } - async pdf(params: channels.PagePdfParams): Promise { + async pdf(params: channels.PagePdfParams, metadata: CallMetadata): Promise { if (!this._page.pdf) throw new Error('PDF generation is only supported for Headless Chromium'); const buffer = await this._page.pdf(params); return { pdf: buffer.toString('base64') }; } - async bringToFront(): Promise { + async bringToFront(params: channels.PageBringToFrontParams, metadata: CallMetadata): Promise { await this._page.bringToFront(); } - async crStartJSCoverage(params: channels.PageCrStartJSCoverageParams): Promise { + async crStartJSCoverage(params: channels.PageCrStartJSCoverageParams, metadata: CallMetadata): Promise { const coverage = this._page.coverage as CRCoverage; await coverage.startJSCoverage(params); } - async crStopJSCoverage(): Promise { + async crStopJSCoverage(params: channels.PageCrStopJSCoverageParams, metadata: CallMetadata): Promise { const coverage = this._page.coverage as CRCoverage; return { entries: await coverage.stopJSCoverage() }; } - async crStartCSSCoverage(params: channels.PageCrStartCSSCoverageParams): Promise { + async crStartCSSCoverage(params: channels.PageCrStartCSSCoverageParams, metadata: CallMetadata): Promise { const coverage = this._page.coverage as CRCoverage; await coverage.startCSSCoverage(params); } - async crStopCSSCoverage(): Promise { + async crStopCSSCoverage(params: channels.PageCrStopCSSCoverageParams, metadata: CallMetadata): Promise { const coverage = this._page.coverage as CRCoverage; return { entries: await coverage.stopCSSCoverage() }; } @@ -256,22 +250,22 @@ export class WorkerDispatcher extends Dispatcher this._dispatchEvent('close')); } - async evaluateExpression(params: channels.WorkerEvaluateExpressionParams): Promise { + async evaluateExpression(params: channels.WorkerEvaluateExpressionParams, metadata: CallMetadata): Promise { return { value: serializeResult(await this._object._evaluateExpression(params.expression, params.isFunction, parseArgument(params.arg))) }; } - async evaluateExpressionHandle(params: channels.WorkerEvaluateExpressionHandleParams): Promise { + async evaluateExpressionHandle(params: channels.WorkerEvaluateExpressionHandleParams, metadata: CallMetadata): Promise { return { handle: createHandle(this._scope, await this._object._evaluateExpressionHandle(params.expression, params.isFunction, parseArgument(params.arg))) }; } } -export class BindingCallDispatcher extends Dispatcher implements channels.BindingCallChannel { +export class BindingCallDispatcher extends Dispatcher<{}, channels.BindingCallInitializer> implements channels.BindingCallChannel { private _resolve: ((arg: any) => void) | undefined; private _reject: ((error: any) => void) | undefined; private _promise: Promise; constructor(scope: DispatcherScope, name: string, needsHandle: boolean, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) { - super(scope, new SdkObject(null), 'BindingCall', { + super(scope, {}, 'BindingCall', { frame: lookupDispatcher(source.frame), name, args: needsHandle ? undefined : args.map(serializeResult), @@ -287,11 +281,11 @@ export class BindingCallDispatcher extends Dispatcher { + async resolve(params: channels.BindingCallResolveParams, metadata: CallMetadata): Promise { this._resolve!(parseArgument(params.result)); } - async reject(params: channels.BindingCallRejectParams): Promise { + async reject(params: channels.BindingCallRejectParams, metadata: CallMetadata): Promise { this._reject!(parseError(params.error)); } } diff --git a/src/dispatchers/streamDispatcher.ts b/src/dispatchers/streamDispatcher.ts index 1495f0e5e5..4e0d777ada 100644 --- a/src/dispatchers/streamDispatcher.ts +++ b/src/dispatchers/streamDispatcher.ts @@ -17,27 +17,18 @@ import * as channels from '../protocol/channels'; import { Dispatcher, DispatcherScope } from './dispatcher'; import * as stream from 'stream'; -import { SdkObject } from '../server/sdkObject'; -export class StreamWrapper extends SdkObject { - readonly stream: stream.Readable; - constructor(parentObject: SdkObject, stream: stream.Readable) { - super(parentObject); - this.stream = stream; - } -} - -export class StreamDispatcher extends Dispatcher implements channels.StreamChannel { - constructor(scope: DispatcherScope, stream: StreamWrapper) { +export class StreamDispatcher extends Dispatcher implements channels.StreamChannel { + constructor(scope: DispatcherScope, stream: stream.Readable) { super(scope, stream, 'Stream', {}); } async read(params: channels.StreamReadParams): Promise { - const buffer = this._object.stream.read(Math.min(this._object.stream.readableLength, params.size || this._object.stream.readableLength)); + const buffer = this._object.read(Math.min(this._object.readableLength, params.size || this._object.readableLength)); return { binary: buffer ? buffer.toString('base64') : '' }; } async close() { - this._object.stream.destroy(); + this._object.destroy(); } } diff --git a/src/server/android/android.ts b/src/server/android/android.ts index 0557f9d2ef..3306a5a170 100644 --- a/src/server/android/android.ts +++ b/src/server/android/android.ts @@ -32,12 +32,12 @@ import { RecentLogsCollector } from '../../utils/debugLogger'; import { TimeoutSettings } from '../../utils/timeoutSettings'; import { AndroidWebView } from '../../protocol/channels'; import { CRPage } from '../chromium/crPage'; -import { SdkObject } from '../sdkObject'; +import { SdkObject, internalCallMetadata } from '../instrumentation'; const readFileAsync = util.promisify(fs.readFile); export interface Backend { - devices(owner: SdkObject): Promise; + devices(): Promise; } export interface DeviceBackend { @@ -45,11 +45,11 @@ export interface DeviceBackend { status: string; close(): Promise; init(): Promise; - runCommand(owner: SdkObject, command: string): Promise; - open(owner: SdkObject, command: string): Promise; + runCommand(command: string): Promise; + open(command: string): Promise; } -export interface SocketBackend extends SdkObject { +export interface SocketBackend extends EventEmitter { write(data: Buffer): Promise; close(): Promise; } @@ -72,7 +72,7 @@ export class Android extends SdkObject { } async devices(): Promise { - const devices = (await this._backend.devices(this)).filter(d => d.status === 'device'); + const devices = (await this._backend.devices()).filter(d => d.status === 'device'); const newSerials = new Set(); for (const d of devices) { newSerials.add(d.serial); @@ -125,7 +125,7 @@ export class AndroidDevice extends SdkObject { static async create(android: Android, backend: DeviceBackend): Promise { await backend.init(); - const model = await backend.runCommand(android, 'shell:getprop ro.product.model'); + const model = await backend.runCommand('shell:getprop ro.product.model'); const device = new AndroidDevice(android, backend, model.toString().trim()); await device._init(); return device; @@ -144,17 +144,17 @@ export class AndroidDevice extends SdkObject { } async shell(command: string): Promise { - const result = await this._backend.runCommand(this, `shell:${command}`); + const result = await this._backend.runCommand(`shell:${command}`); await this._refreshWebViews(); return result; } async open(command: string): Promise { - return await this._backend.open(this, `${command}`); + return await this._backend.open(`${command}`); } async screenshot(): Promise { - return await this._backend.runCommand(this, `shell:screencap -p`); + return await this._backend.runCommand(`shell:screencap -p`); } private async _driver(): Promise { @@ -199,7 +199,7 @@ export class AndroidDevice extends SdkObject { debug('pw:android')(`Polling the socket localabstract:${socketName}`); while (!socket) { try { - socket = await this._backend.open(this, `localabstract:${socketName}`); + socket = await this._backend.open(`localabstract:${socketName}`); } catch (e) { await new Promise(f => setTimeout(f, 250)); } @@ -235,13 +235,13 @@ export class AndroidDevice extends SdkObject { async launchBrowser(pkg: string = 'com.android.chrome', options: types.BrowserContextOptions = {}): Promise { debug('pw:android')('Force-stopping', pkg); - await this._backend.runCommand(this, `shell:am force-stop ${pkg}`); + await this._backend.runCommand(`shell:am force-stop ${pkg}`); const socketName = 'playwright-' + createGuid(); const commandLine = `_ --disable-fre --no-default-browser-check --no-first-run --remote-debugging-socket-name=${socketName}`; debug('pw:android')('Starting', pkg, commandLine); - await this._backend.runCommand(this, `shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`); - await this._backend.runCommand(this, `shell:am start -n ${pkg}/com.google.android.apps.chrome.Main about:blank`); + await this._backend.runCommand(`shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`); + await this._backend.runCommand(`shell:am start -n ${pkg}/com.google.android.apps.chrome.Main about:blank`); return await this._connectToBrowser(socketName, options); } @@ -273,7 +273,7 @@ export class AndroidDevice extends SdkObject { validateBrowserContextOptions(options, browserOptions); const browser = await CRBrowser.connect(androidBrowser, browserOptions); - const controller = new ProgressController(); + const controller = new ProgressController(internalCallMetadata(), this); const defaultContext = browser._defaultContext!; await controller.run(async progress => { await defaultContext._loadDefaultContextAsIs(progress); @@ -296,7 +296,7 @@ export class AndroidDevice extends SdkObject { async installApk(content: Buffer, options?: { args?: string[] }): Promise { const args = options && options.args ? options.args : ['-r', '-t', '-S']; debug('pw:android')('Opening install socket'); - const installSocket = await this._backend.open(this, `shell:cmd package install ${args.join(' ')} ${content.length}`); + const installSocket = await this._backend.open(`shell:cmd package install ${args.join(' ')} ${content.length}`); debug('pw:android')('Writing driver bytes: ' + content.length); await installSocket.write(content); const success = await new Promise(f => installSocket.on('data', f)); @@ -305,7 +305,7 @@ export class AndroidDevice extends SdkObject { } async push(content: Buffer, path: string, mode = 0o644): Promise { - const socket = await this._backend.open(this, `sync:`); + const socket = await this._backend.open(`sync:`); const sendHeader = async (command: string, length: number) => { const buffer = Buffer.alloc(command.length + 4); buffer.write(command, 0); @@ -329,7 +329,7 @@ export class AndroidDevice extends SdkObject { } private async _refreshWebViews() { - const sockets = (await this._backend.runCommand(this, `shell:cat /proc/net/unix | grep webview_devtools_remote`)).toString().split('\n'); + const sockets = (await this._backend.runCommand(`shell:cat /proc/net/unix | grep webview_devtools_remote`)).toString().split('\n'); if (this._isClosed) return; @@ -345,7 +345,7 @@ export class AndroidDevice extends SdkObject { if (this._webViews.has(pid)) continue; - const procs = (await this._backend.runCommand(this, `shell:ps -A | grep ${pid}`)).toString().split('\n'); + const procs = (await this._backend.runCommand(`shell:ps -A | grep ${pid}`)).toString().split('\n'); if (this._isClosed) return; let pkg = ''; diff --git a/src/server/android/backendAdb.ts b/src/server/android/backendAdb.ts index b73bb3566d..4e7f3b8a52 100644 --- a/src/server/android/backendAdb.ts +++ b/src/server/android/backendAdb.ts @@ -17,12 +17,12 @@ import * as assert from 'assert'; import * as debug from 'debug'; import * as net from 'net'; -import { SdkObject } from '../sdkObject'; +import { EventEmitter } from 'events'; import { Backend, DeviceBackend, SocketBackend } from './android'; export class AdbBackend implements Backend { - async devices(sdkObject: SdkObject): Promise { - const result = await runCommand(sdkObject, 'host:devices'); + async devices(): Promise { + const result = await runCommand('host:devices'); const lines = result.toString().trim().split('\n'); return lines.map(line => { const [serial, status] = line.trim().split('\t'); @@ -46,20 +46,20 @@ class AdbDevice implements DeviceBackend { async close() { } - runCommand(sdkObject: SdkObject, command: string): Promise { - return runCommand(sdkObject, command, this.serial); + runCommand(command: string): Promise { + return runCommand(command, this.serial); } - async open(sdkObject: SdkObject, command: string): Promise { - const result = await open(sdkObject, command, this.serial); + async open(command: string): Promise { + const result = await open(command, this.serial); result.becomeSocket(); return result; } } -async function runCommand(sdkObject: SdkObject, command: string, serial?: string): Promise { +async function runCommand(command: string, serial?: string): Promise { debug('pw:adb:runCommand')(command, serial); - const socket = new BufferedSocketWrapper(sdkObject, command, net.createConnection({ port: 5037 })); + const socket = new BufferedSocketWrapper(command, net.createConnection({ port: 5037 })); if (serial) { await socket.write(encodeMessage(`host:transport:${serial}`)); const status = await socket.read(4); @@ -79,8 +79,8 @@ async function runCommand(sdkObject: SdkObject, command: string, serial?: string return commandOutput; } -async function open(sdkObject: SdkObject, command: string, serial?: string): Promise { - const socket = new BufferedSocketWrapper(sdkObject, command, net.createConnection({ port: 5037 })); +async function open(command: string, serial?: string): Promise { + const socket = new BufferedSocketWrapper(command, net.createConnection({ port: 5037 })); if (serial) { await socket.write(encodeMessage(`host:transport:${serial}`)); const status = await socket.read(4); @@ -98,7 +98,7 @@ function encodeMessage(message: string): Buffer { return Buffer.from(lenHex + message); } -class BufferedSocketWrapper extends SdkObject implements SocketBackend { +class BufferedSocketWrapper extends EventEmitter implements SocketBackend { private _socket: net.Socket; private _buffer = Buffer.from([]); private _isSocket = false; @@ -107,8 +107,8 @@ class BufferedSocketWrapper extends SdkObject implements SocketBackend { private _isClosed = false; private _command: string; - constructor(parent: SdkObject, command: string, socket: net.Socket) { - super(parent); + constructor(command: string, socket: net.Socket) { + super(); this._command = command; this._socket = socket; this._connectPromise = new Promise(f => this._socket.on('connect', f)); diff --git a/src/server/browser.ts b/src/server/browser.ts index a51e7b4104..88f3eac5d8 100644 --- a/src/server/browser.ts +++ b/src/server/browser.ts @@ -15,15 +15,14 @@ */ import * as types from './types'; -import { BrowserContext, ContextListener, Video } from './browserContext'; +import { BrowserContext, Video } from './browserContext'; import { Page } from './page'; import { Download } from './download'; import { ProxySettings } from './types'; import { ChildProcess } from 'child_process'; import { RecentLogsCollector } from '../utils/debugLogger'; import * as registry from '../utils/registry'; -import { SdkObject } from './sdkObject'; -import { Selectors } from './selectors'; +import { SdkObject } from './instrumentation'; export interface BrowserProcess { onclose: ((exitCode: number | null, signal: string | null) => void) | undefined; @@ -33,12 +32,9 @@ export interface BrowserProcess { } export type PlaywrightOptions = { - contextListeners: ContextListener[], registry: registry.Registry, isInternal: boolean, rootSdkObject: SdkObject, - // FIXME, this is suspicious - selectors: Selectors }; export type BrowserOptions = PlaywrightOptions & { diff --git a/src/server/browserContext.ts b/src/server/browserContext.ts index 5089450631..abb27ac499 100644 --- a/src/server/browserContext.ts +++ b/src/server/browserContext.ts @@ -18,17 +18,16 @@ import { TimeoutSettings } from '../utils/timeoutSettings'; import { mkdirIfNeeded } from '../utils/utils'; import { Browser, BrowserOptions } from './browser'; -import * as dom from './dom'; import { Download } from './download'; import * as frames from './frames'; import { helper } from './helper'; import * as network from './network'; import { Page, PageBinding, PageDelegate } from './page'; -import { Progress, ProgressController, ProgressResult } from './progress'; -import { Selectors } from './selectors'; +import { Progress } from './progress'; +import { Selectors, serverSelectors } from './selectors'; import * as types from './types'; import * as path from 'path'; -import { SdkObject } from './sdkObject'; +import { CallMetadata, SdkObject } from './instrumentation'; export class Video { readonly _videoId: string; @@ -58,42 +57,6 @@ export class Video { } } -export type ActionMetadata = { - type: 'click' | 'fill' | 'dblclick' | 'hover' | 'selectOption' | 'setInputFiles' | 'type' | 'press' | 'check' | 'uncheck' | 'goto' | 'setContent' | 'goBack' | 'goForward' | 'reload' | 'tap', - page: Page, - target?: dom.ElementHandle | string, - value?: string, - stack?: string, -}; - -export interface ActionListener { - onActionCheckpoint(name: string, metadata: ActionMetadata): Promise; - onAfterAction(result: ProgressResult, metadata: ActionMetadata): Promise; -} - -export async function runAction(task: (controller: ProgressController) => Promise, metadata: ActionMetadata): Promise { - const controller = new ProgressController(); - controller.setListener({ - onProgressCheckpoint: async (name: string): Promise => { - for (const listener of metadata.page._browserContext._actionListeners) - await listener.onActionCheckpoint(name, metadata); - }, - - onProgressDone: async (result: ProgressResult): Promise => { - for (const listener of metadata.page._browserContext._actionListeners) - await listener.onAfterAction(result, metadata); - }, - }); - const result = await task(controller); - return result; -} - -export interface ContextListener { - onContextCreated(context: BrowserContext): Promise; - onContextWillDestroy(context: BrowserContext): Promise; - onContextDidDestroy(context: BrowserContext): Promise; -} - export abstract class BrowserContext extends SdkObject { static Events = { Close: 'close', @@ -117,7 +80,6 @@ export abstract class BrowserContext extends SdkObject { readonly _browser: Browser; readonly _browserContextId: string | undefined; private _selectors?: Selectors; - readonly _actionListeners = new Set(); private _origins = new Set(); terminalSize: { rows?: number, columns?: number } = {}; @@ -136,12 +98,11 @@ export abstract class BrowserContext extends SdkObject { } selectors(): Selectors { - return this._selectors || this._browser.options.selectors; + return this._selectors || serverSelectors; } async _initialize() { - for (const listener of this._browser.options.contextListeners) - await listener.onContextCreated(this); + await this.instrumentation.onContextCreated(this); } async _ensureVideosPath() { @@ -292,8 +253,7 @@ export abstract class BrowserContext extends SdkObject { this.emit(BrowserContext.Events.BeforeClose); this._closedStatus = 'closing'; - for (const listener of this._browser.options.contextListeners) - await listener.onContextWillDestroy(this); + await this.instrumentation.onContextWillDestroy(this); // Collect videos/downloads that we will await. const promises: Promise[] = []; @@ -321,8 +281,7 @@ export abstract class BrowserContext extends SdkObject { await this._browser.close(); // Bookkeeping. - for (const listener of this._browser.options.contextListeners) - await listener.onContextDidDestroy(this); + await this.instrumentation.onContextWillDestroy(this); this._didCloseInternal(); } await this._closePromise; @@ -343,7 +302,7 @@ export abstract class BrowserContext extends SdkObject { this._origins.add(origin); } - async storageState(): Promise { + async storageState(metadata: CallMetadata): Promise { const result: types.StorageState = { cookies: (await this.cookies()).filter(c => c.value !== ''), origins: [] @@ -357,7 +316,7 @@ export abstract class BrowserContext extends SdkObject { const originStorage: types.OriginStorage = { origin, localStorage: [] }; result.origins.push(originStorage); const frame = page.mainFrame(); - await frame.goto(new ProgressController(), origin); + await frame.goto(metadata, origin); const storage = await frame._evaluateExpression(`({ localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })), })`, false, undefined, 'utility'); @@ -368,7 +327,7 @@ export abstract class BrowserContext extends SdkObject { return result; } - async setStorageState(state: types.SetStorageState) { + async setStorageState(metadata: CallMetadata, state: types.SetStorageState) { if (state.cookies) await this.addCookies(state.cookies); if (state.origins && state.origins.length) { @@ -378,7 +337,7 @@ export abstract class BrowserContext extends SdkObject { }); for (const originState of state.origins) { const frame = page.mainFrame(); - await frame.goto(new ProgressController(), originState.origin); + await frame.goto(metadata, originState.origin); await frame._evaluateExpression(` originState => { for (const { name, value } of (originState.localStorage || [])) diff --git a/src/server/browserType.ts b/src/server/browserType.ts index 2b87f07f93..8536365f03 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -31,7 +31,7 @@ import { validateHostRequirements } from './validateDependencies'; import { isDebugMode } from '../utils/utils'; import { helper } from './helper'; import { RecentLogsCollector } from '../utils/debugLogger'; -import { SdkObject } from './sdkObject'; +import { CallMetadata, SdkObject } from './instrumentation'; const mkdirAsync = util.promisify(fs.mkdir); const mkdtempAsync = util.promisify(fs.mkdtemp); @@ -59,9 +59,9 @@ export abstract class BrowserType extends SdkObject { return this._name; } - async launch(options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise { + async launch(metadata: CallMetadata, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise { options = validateLaunchOptions(options); - const controller = new ProgressController(); + const controller = new ProgressController(metadata, this); controller.setLogName('browser'); const browser = await controller.run(progress => { return this._innerLaunchWithRetries(progress, options, undefined, helper.debugProtocolLogger(protocolLogger)).catch(e => { throw this._rewriteStartupError(e); }); @@ -69,10 +69,10 @@ export abstract class BrowserType extends SdkObject { return browser; } - async launchPersistentContext(userDataDir?: string, options: types.LaunchPersistentOptions = {}): Promise { + async launchPersistentContext(metadata: CallMetadata, userDataDir?: string, options: types.LaunchPersistentOptions = {}): Promise { options = validateLaunchOptions(options); + const controller = new ProgressController(metadata, this); const persistent: types.BrowserContextOptions = options; - const controller = new ProgressController(); controller.setLogName('browser'); const browser = await controller.run(progress => { return this._innerLaunchWithRetries(progress, options, persistent, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupError(e); }); diff --git a/src/server/chromium/crConnection.ts b/src/server/chromium/crConnection.ts index 1c3dde03c8..c590a3acce 100644 --- a/src/server/chromium/crConnection.ts +++ b/src/server/chromium/crConnection.ts @@ -23,7 +23,6 @@ import { rewriteErrorMessage } from '../../utils/stackTrace'; import { debugLogger, RecentLogsCollector } from '../../utils/debugLogger'; import { ProtocolLogger } from '../types'; import { helper } from '../helper'; -import { SdkObject } from '../sdkObject'; export const ConnectionEvents = { Disconnected: Symbol('ConnectionEvents.Disconnected') @@ -124,7 +123,7 @@ export const CRSessionEvents = { Disconnected: Symbol('Events.CDPSession.Disconnected') }; -export class CRSession extends SdkObject { +export class CRSession extends EventEmitter { _connection: CRConnection | null; _eventListener?: (method: string, params?: Object) => void; private readonly _callbacks = new Map void, reject: (e: Error) => void, error: Error, method: string}>(); @@ -140,7 +139,8 @@ export class CRSession extends SdkObject { once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; constructor(connection: CRConnection, rootSessionId: string, targetType: string, sessionId: string) { - super(null); + super(); + this.setMaxListeners(0); this._connection = connection; this._rootSessionId = rootSessionId; this._targetType = targetType; diff --git a/src/server/chromium/crPage.ts b/src/server/chromium/crPage.ts index b5911c2d11..e3a342c4ac 100644 --- a/src/server/chromium/crPage.ts +++ b/src/server/chromium/crPage.ts @@ -759,7 +759,7 @@ class FrameSession { lineNumber: lineNumber || 0, columnNumber: 0, }; - this._page.emit(Page.Events.Console, new ConsoleMessage(this._page, level, text, [], location)); + this._page.emit(Page.Events.Console, new ConsoleMessage(level, text, [], location)); } } @@ -814,7 +814,7 @@ class FrameSession { const ffmpegPath = this._crPage._browserContext._browser.options.registry.executablePath('ffmpeg'); if (!ffmpegPath) throw new Error('ffmpeg executable was not found'); - this._videoRecorder = await VideoRecorder.launch(ffmpegPath, options); + this._videoRecorder = await VideoRecorder.launch(this._crPage._page, ffmpegPath, options); this._screencastId = screencastId; const gotFirstFrame = new Promise(f => this._client.once('Page.screencastFrame', f)); await this._client.send('Page.startScreencast', { diff --git a/src/server/chromium/videoRecorder.ts b/src/server/chromium/videoRecorder.ts index dcc41db76c..dba5e27f91 100644 --- a/src/server/chromium/videoRecorder.ts +++ b/src/server/chromium/videoRecorder.ts @@ -16,8 +16,10 @@ import { ChildProcess } from 'child_process'; import { assert, monotonicTime } from '../../utils/utils'; +import { Page } from '../page'; import { launchProcess } from '../processLauncher'; import { Progress, ProgressController } from '../progress'; +import { internalCallMetadata } from '../instrumentation'; import * as types from '../types'; const fps = 25; @@ -34,11 +36,11 @@ export class VideoRecorder { private _isStopped = false; private _ffmpegPath: string; - static async launch(ffmpegPath: string, options: types.PageScreencastOptions): Promise { + static async launch(page: Page, ffmpegPath: string, options: types.PageScreencastOptions): Promise { if (!options.outputFile.endsWith('.webm')) throw new Error('File must have .webm extension'); - const controller = new ProgressController(); + const controller = new ProgressController(internalCallMetadata(), page); controller.setLogName('browser'); return await controller.run(async progress => { const recorder = new VideoRecorder(ffmpegPath, progress); diff --git a/src/server/console.ts b/src/server/console.ts index f4a2ad66a6..08b8367df9 100644 --- a/src/server/console.ts +++ b/src/server/console.ts @@ -15,17 +15,15 @@ */ import * as js from './javascript'; -import { SdkObject } from './sdkObject'; import { ConsoleMessageLocation } from './types'; -export class ConsoleMessage extends SdkObject { +export class ConsoleMessage { private _type: string; private _text?: string; private _args: js.JSHandle[]; private _location: ConsoleMessageLocation; - constructor(parent: SdkObject, type: string, text: string | undefined, args: js.JSHandle[], location?: ConsoleMessageLocation) { - super(parent); + constructor(type: string, text: string | undefined, args: js.JSHandle[], location?: ConsoleMessageLocation) { this._type = type; this._text = text; this._args = args; diff --git a/src/server/dialog.ts b/src/server/dialog.ts index f10cc1b239..c0dd4755b2 100644 --- a/src/server/dialog.ts +++ b/src/server/dialog.ts @@ -17,7 +17,7 @@ import { assert } from '../utils/utils'; import { Page } from './page'; -import { SdkObject } from './sdkObject'; +import { SdkObject } from './instrumentation'; type OnHandle = (accept: boolean, promptText?: string) => Promise; diff --git a/src/server/dom.ts b/src/server/dom.ts index 405e1684d2..9717542881 100644 --- a/src/server/dom.ts +++ b/src/server/dom.ts @@ -22,8 +22,9 @@ import * as js from './javascript'; import { Page } from './page'; import { SelectorInfo } from './selectors'; import * as types from './types'; -import { Progress, ProgressController, runAbortableTask } from './progress'; +import { Progress, ProgressController } from './progress'; import { FatalDOMError, RetargetableDOMError } from './common/domErrors'; +import { CallMetadata } from './instrumentation'; export class FrameExecutionContext extends js.ExecutionContext { readonly frame: frames.Frame; @@ -214,8 +215,9 @@ export class ElementHandle extends js.JSHandle { } } - async scrollIntoViewIfNeeded(options: types.TimeoutOptions = {}) { - return runAbortableTask( + async scrollIntoViewIfNeeded(metadata: CallMetadata, options: types.TimeoutOptions = {}) { + const controller = new ProgressController(metadata, this); + return controller.run( progress => this._waitAndScrollIntoViewIfNeeded(progress), this._page._timeoutSettings.timeout(options)); } @@ -392,7 +394,8 @@ export class ElementHandle extends js.JSHandle { return 'done'; } - async hover(controller: ProgressController, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise { + async hover(metadata: CallMetadata, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._hover(progress, options); return assertDone(throwRetargetableDOMError(result)); @@ -403,7 +406,8 @@ export class ElementHandle extends js.JSHandle { return this._retryPointerAction(progress, 'hover', false /* waitForEnabled */, point => this._page.mouse.move(point.x, point.y), options); } - async click(controller: ProgressController, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise { + async click(metadata: CallMetadata, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._click(progress, options); return assertDone(throwRetargetableDOMError(result)); @@ -414,7 +418,8 @@ export class ElementHandle extends js.JSHandle { return this._retryPointerAction(progress, 'click', true /* waitForEnabled */, point => this._page.mouse.click(point.x, point.y, options), options); } - async dblclick(controller: ProgressController, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise { + async dblclick(metadata: CallMetadata, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._dblclick(progress, options); return assertDone(throwRetargetableDOMError(result)); @@ -425,7 +430,8 @@ export class ElementHandle extends js.JSHandle { return this._retryPointerAction(progress, 'dblclick', true /* waitForEnabled */, point => this._page.mouse.dblclick(point.x, point.y, options), options); } - async tap(controller: ProgressController, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise { + async tap(metadata: CallMetadata, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._tap(progress, options); return assertDone(throwRetargetableDOMError(result)); @@ -436,7 +442,8 @@ export class ElementHandle extends js.JSHandle { return this._retryPointerAction(progress, 'tap', true /* waitForEnabled */, point => this._page.touchscreen.tap(point.x, point.y), options); } - async selectOption(controller: ProgressController, elements: ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions): Promise { + async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions): Promise { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._selectOption(progress, elements, values, options); return throwRetargetableDOMError(result); @@ -457,7 +464,8 @@ export class ElementHandle extends js.JSHandle { }); } - async fill(controller: ProgressController, value: string, options: types.NavigatingActionWaitOptions = {}): Promise { + async fill(metadata: CallMetadata, value: string, options: types.NavigatingActionWaitOptions = {}): Promise { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._fill(progress, value, options); assertDone(throwRetargetableDOMError(result)); @@ -491,8 +499,9 @@ export class ElementHandle extends js.JSHandle { }, 'input'); } - async selectText(options: types.TimeoutOptions = {}): Promise { - return runAbortableTask(async progress => { + async selectText(metadata: CallMetadata, options: types.TimeoutOptions = {}): Promise { + const controller = new ProgressController(metadata, this); + return controller.run(async progress => { progress.throwIfAborted(); // Avoid action that has side-effects. const poll = await this._evaluateHandleInUtility(([injected, node]) => { return injected.waitForVisibleAndSelectText(node); @@ -503,7 +512,8 @@ export class ElementHandle extends js.JSHandle { }, this._page._timeoutSettings.timeout(options)); } - async setInputFiles(controller: ProgressController, files: types.FilePayload[], options: types.NavigatingActionWaitOptions) { + async setInputFiles(metadata: CallMetadata, files: types.FilePayload[], options: types.NavigatingActionWaitOptions) { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._setInputFiles(progress, files, options); return assertDone(throwRetargetableDOMError(result)); @@ -531,8 +541,9 @@ export class ElementHandle extends js.JSHandle { return 'done'; } - async focus(): Promise { - await runAbortableTask(async progress => { + async focus(metadata: CallMetadata): Promise { + const controller = new ProgressController(metadata, this); + await controller.run(async progress => { const result = await this._focus(progress); await this._page._doSlowMo(); return assertDone(throwRetargetableDOMError(result)); @@ -545,7 +556,8 @@ export class ElementHandle extends js.JSHandle { return throwFatalDOMError(result); } - async type(controller: ProgressController, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise { + async type(metadata: CallMetadata, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._type(progress, text, options); return assertDone(throwRetargetableDOMError(result)); @@ -565,7 +577,8 @@ export class ElementHandle extends js.JSHandle { }, 'input'); } - async press(controller: ProgressController, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise { + async press(metadata: CallMetadata, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._press(progress, key, options); return assertDone(throwRetargetableDOMError(result)); @@ -585,14 +598,16 @@ export class ElementHandle extends js.JSHandle { }, 'input'); } - async check(controller: ProgressController, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + async check(metadata: CallMetadata, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._setChecked(progress, true, options); return assertDone(throwRetargetableDOMError(result)); }, this._page._timeoutSettings.timeout(options)); } - async uncheck(controller: ProgressController, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + async uncheck(metadata: CallMetadata, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._setChecked(progress, false, options); return assertDone(throwRetargetableDOMError(result)); @@ -614,8 +629,9 @@ export class ElementHandle extends js.JSHandle { return this._page._delegate.getBoundingBox(this); } - async screenshot(options: types.ElementScreenshotOptions = {}): Promise { - return runAbortableTask( + async screenshot(metadata: CallMetadata, options: types.ElementScreenshotOptions = {}): Promise { + const controller = new ProgressController(metadata, this); + return controller.run( progress => this._page._screenshotter.screenshotElement(progress, this, options), this._page._timeoutSettings.timeout(options)); } @@ -679,8 +695,9 @@ export class ElementHandle extends js.JSHandle { }, {}); } - async waitForElementState(state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled' | 'editable', options: types.TimeoutOptions = {}): Promise { - return runAbortableTask(async progress => { + async waitForElementState(metadata: CallMetadata, state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled' | 'editable', options: types.TimeoutOptions = {}): Promise { + const controller = new ProgressController(metadata, this); + return controller.run(async progress => { progress.log(` waiting for element to be ${state}`); if (state === 'visible') { const poll = await this._evaluateHandleInUtility(([injected, node]) => { @@ -735,13 +752,14 @@ export class ElementHandle extends js.JSHandle { }, this._page._timeoutSettings.timeout(options)); } - async waitForSelector(selector: string, options: types.WaitForElementOptions = {}): Promise | null> { + async waitForSelector(metadata: CallMetadata, selector: string, options: types.WaitForElementOptions = {}): Promise | null> { const { state = 'visible' } = options; if (!['attached', 'detached', 'visible', 'hidden'].includes(state)) throw new Error(`state: expected one of (attached|detached|visible|hidden)`); const info = this._page.selectors._parseSelector(selector); const task = waitForSelectorTask(info, state, this); - return runAbortableTask(async progress => { + const controller = new ProgressController(metadata, this); + return controller.run(async progress => { progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`); const context = await this._context.frame._context(info.world); const injected = await context.injectedScript(); diff --git a/src/server/download.ts b/src/server/download.ts index 1cbf7a2c47..687283bce1 100644 --- a/src/server/download.ts +++ b/src/server/download.ts @@ -19,11 +19,10 @@ import * as fs from 'fs'; import * as util from 'util'; import { Page } from './page'; import { assert } from '../utils/utils'; -import { SdkObject } from './sdkObject'; type SaveCallback = (localPath: string, error?: string) => Promise; -export class Download extends SdkObject { +export class Download { private _downloadsPath: string; private _uuid: string; private _finishedCallback: () => void; @@ -38,7 +37,6 @@ export class Download extends SdkObject { private _suggestedFilename: string | undefined; constructor(page: Page, downloadsPath: string, uuid: string, url: string, suggestedFilename?: string) { - super(page); this._page = page; this._downloadsPath = downloadsPath; this._uuid = uuid; diff --git a/src/server/electron/electron.ts b/src/server/electron/electron.ts index dace10389d..851104f51b 100644 --- a/src/server/electron/electron.ts +++ b/src/server/electron/electron.ts @@ -26,13 +26,13 @@ import * as types from '../types'; import { launchProcess, envArrayToObject } from '../processLauncher'; import { BrowserContext } from '../browserContext'; import type {BrowserWindow} from 'electron'; -import { Progress, ProgressController, runAbortableTask } from '../progress'; +import { Progress, ProgressController } from '../progress'; import { helper } from '../helper'; import { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser'; import * as childProcess from 'child_process'; import * as readline from 'readline'; import { RecentLogsCollector } from '../../utils/debugLogger'; -import { SdkObject } from '../sdkObject'; +import { internalCallMetadata, SdkObject } from '../instrumentation'; export type ElectronLaunchOptionsBase = { executablePath?: string, @@ -89,7 +89,8 @@ export class ElectronApplication extends SdkObject { if (!handle) return; page.browserWindow = handle; - await runAbortableTask(progress => page.mainFrame()._waitForLoadState(progress, 'domcontentloaded'), page._timeoutSettings.navigationTimeout({})).catch(e => {}); // can happen after detach + const controller = new ProgressController(internalCallMetadata(), this); + await controller.run(progress => page.mainFrame()._waitForLoadState(progress, 'domcontentloaded'), page._timeoutSettings.navigationTimeout({})).catch(e => {}); // can happen after detach this.emit(ElectronApplication.Events.Window, page); } @@ -105,7 +106,7 @@ export class ElectronApplication extends SdkObject { } private async _waitForEvent(event: string, predicate?: Function): Promise { - const progressController = new ProgressController(); + const progressController = new ProgressController(internalCallMetadata(), this); if (event !== ElectronApplication.Events.Close) this._browserContext._closePromise.then(error => progressController.abort(error)); return progressController.run(progress => helper.waitForEvent(progress, this, event, predicate).promise, this._timeoutSettings.timeout({})); @@ -133,7 +134,7 @@ export class Electron extends SdkObject { const { args = [], } = options; - const controller = new ProgressController(); + const controller = new ProgressController(internalCallMetadata(), this); controller.setLogName('browser'); return controller.run(async progress => { let app: ElectronApplication | undefined = undefined; diff --git a/src/server/frames.ts b/src/server/frames.ts index 73bff8e193..f0f14d08ec 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -23,10 +23,10 @@ import * as network from './network'; import { Page } from './page'; import * as types from './types'; import { BrowserContext } from './browserContext'; -import { Progress, ProgressController, runAbortableTask } from './progress'; +import { Progress, ProgressController } from './progress'; import { assert, makeWaitForNextTask } from '../utils/utils'; import { debugLogger } from '../utils/debugLogger'; -import { SdkObject } from './sdkObject'; +import { CallMetadata, SdkObject } from './instrumentation'; type ContextData = { contextPromise: Promise; @@ -480,14 +480,16 @@ export class Frame extends SdkObject { this._subtreeLifecycleEvents = events; } - setupNavigationProgressController(controller: ProgressController) { + setupNavigationProgressController(metadata: CallMetadata): ProgressController { + const controller = new ProgressController(metadata, this); this._page._disconnectedPromise.then(() => controller.abort(new Error('Navigation failed because page was closed!'))); this._page._crashedPromise.then(() => controller.abort(new Error('Navigation failed because page crashed!'))); this._detachedPromise.then(() => controller.abort(new Error('Navigating frame was detached!'))); + return controller; } - async goto(controller: ProgressController, url: string, options: types.GotoOptions = {}): Promise { - this.setupNavigationProgressController(controller); + async goto(metadata: CallMetadata, url: string, options: types.GotoOptions = {}): Promise { + const controller = this.setupNavigationProgressController(metadata); return controller.run(async progress => { const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); progress.log(`navigating to "${url}", waiting until "${waitUntil}"`); @@ -599,7 +601,8 @@ export class Frame extends SdkObject { return this._page.selectors._query(this, selector); } - async waitForSelector(selector: string, options: types.WaitForElementOptions = {}): Promise | null> { + async waitForSelector(metadata: CallMetadata, selector: string, options: types.WaitForElementOptions = {}): Promise | null> { + const controller = new ProgressController(metadata, this); if ((options as any).visibility) throw new Error('options.visibility is not supported, did you mean options.state?'); if ((options as any).waitFor && (options as any).waitFor !== 'visible') @@ -609,7 +612,7 @@ export class Frame extends SdkObject { throw new Error(`state: expected one of (attached|detached|visible|hidden)`); const info = this._page.selectors._parseSelector(selector); const task = dom.waitForSelectorTask(info, state); - return runAbortableTask(async progress => { + return controller.run(async progress => { progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`); const result = await this._scheduleRerunnableHandleTask(progress, info.world, task); if (!result.asElement()) { @@ -621,10 +624,11 @@ export class Frame extends SdkObject { }, this._page._timeoutSettings.timeout(options)); } - async dispatchEvent(selector: string, type: string, eventInit?: Object, options: types.TimeoutOptions = {}): Promise { + async dispatchEvent(metadata: CallMetadata, selector: string, type: string, eventInit?: Object, options: types.TimeoutOptions = {}): Promise { + const controller = new ProgressController(metadata, this); const info = this._page.selectors._parseSelector(selector); const task = dom.dispatchEventTask(info, type, eventInit || {}); - await runAbortableTask(async progress => { + await controller.run(async progress => { progress.log(`Dispatching "${type}" event on selector "${selector}"...`); // Note: we always dispatch events in the main world. await this._scheduleRerunnableTask(progress, 'main', task); @@ -664,8 +668,8 @@ export class Frame extends SdkObject { }); } - async setContent(controller: ProgressController, html: string, options: types.NavigateOptions = {}): Promise { - this.setupNavigationProgressController(controller); + async setContent(metadata: CallMetadata, html: string, options: types.NavigateOptions = {}): Promise { + const controller = this.setupNavigationProgressController(metadata); return controller.run(async progress => { const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil; progress.log(`setting frame content, waiting until "${waitUntil}"`); @@ -857,166 +861,188 @@ export class Frame extends SdkObject { } private async _retryWithSelectorIfNotConnected( + controller: ProgressController, selector: string, options: types.TimeoutOptions, action: (progress: Progress, handle: dom.ElementHandle) => Promise): Promise { - return runAbortableTask(async progress => { + return controller.run(async progress => { return this._retryWithProgressIfNotConnected(progress, selector, handle => action(progress, handle)); }, this._page._timeoutSettings.timeout(options)); } - async click(controller: ProgressController, selector: string, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + async click(metadata: CallMetadata, selector: string, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._click(progress, options))); }, this._page._timeoutSettings.timeout(options)); } - async dblclick(controller: ProgressController, selector: string, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + async dblclick(metadata: CallMetadata, selector: string, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._dblclick(progress, options))); }, this._page._timeoutSettings.timeout(options)); } - async tap(controller: ProgressController, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + async tap(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._tap(progress, options))); }, this._page._timeoutSettings.timeout(options)); } - async fill(controller: ProgressController, selector: string, value: string, options: types.NavigatingActionWaitOptions) { + async fill(metadata: CallMetadata, selector: string, value: string, options: types.NavigatingActionWaitOptions) { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._fill(progress, value, options))); }, this._page._timeoutSettings.timeout(options)); } - async focus(selector: string, options: types.TimeoutOptions = {}) { - await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._focus(progress)); + async focus(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}) { + const controller = new ProgressController(metadata, this); + await this._retryWithSelectorIfNotConnected(controller, selector, options, (progress, handle) => handle._focus(progress)); await this._page._doSlowMo(); } - async textContent(selector: string, options: types.TimeoutOptions = {}): Promise { + async textContent(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise { + const controller = new ProgressController(metadata, this); const info = this._page.selectors._parseSelector(selector); const task = dom.textContentTask(info); - return runAbortableTask(async progress => { + return controller.run(async progress => { progress.log(` retrieving textContent from "${selector}"`); return this._scheduleRerunnableTask(progress, info.world, task); }, this._page._timeoutSettings.timeout(options)); } - async innerText(selector: string, options: types.TimeoutOptions = {}): Promise { + async innerText(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise { + const controller = new ProgressController(metadata, this); const info = this._page.selectors._parseSelector(selector); const task = dom.innerTextTask(info); - return runAbortableTask(async progress => { + return controller.run(async progress => { progress.log(` retrieving innerText from "${selector}"`); const result = dom.throwFatalDOMError(await this._scheduleRerunnableTask(progress, info.world, task)); return result.innerText; }, this._page._timeoutSettings.timeout(options)); } - async innerHTML(selector: string, options: types.TimeoutOptions = {}): Promise { + async innerHTML(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise { + const controller = new ProgressController(metadata, this); const info = this._page.selectors._parseSelector(selector); const task = dom.innerHTMLTask(info); - return runAbortableTask(async progress => { + return controller.run(async progress => { progress.log(` retrieving innerHTML from "${selector}"`); return this._scheduleRerunnableTask(progress, info.world, task); }, this._page._timeoutSettings.timeout(options)); } - async getAttribute(selector: string, name: string, options: types.TimeoutOptions = {}): Promise { + async getAttribute(metadata: CallMetadata, selector: string, name: string, options: types.TimeoutOptions = {}): Promise { + const controller = new ProgressController(metadata, this); const info = this._page.selectors._parseSelector(selector); const task = dom.getAttributeTask(info, name); - return runAbortableTask(async progress => { + return controller.run(async progress => { progress.log(` retrieving attribute "${name}" from "${selector}"`); return this._scheduleRerunnableTask(progress, info.world, task); }, this._page._timeoutSettings.timeout(options)); } - async isVisible(selector: string, options: types.TimeoutOptions = {}): Promise { + async isVisible(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise { + const controller = new ProgressController(metadata, this); const info = this._page.selectors._parseSelector(selector); const task = dom.visibleTask(info); - return runAbortableTask(async progress => { + return controller.run(async progress => { progress.log(` checking visibility of "${selector}"`); return this._scheduleRerunnableTask(progress, info.world, task); }, this._page._timeoutSettings.timeout(options)); } - async isHidden(selector: string, options: types.TimeoutOptions = {}): Promise { - return !(await this.isVisible(selector, options)); + async isHidden(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise { + return !(await this.isVisible(metadata, selector, options)); } - async isDisabled(selector: string, options: types.TimeoutOptions = {}): Promise { + async isDisabled(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise { + const controller = new ProgressController(metadata, this); const info = this._page.selectors._parseSelector(selector); const task = dom.disabledTask(info); - return runAbortableTask(async progress => { + return controller.run(async progress => { progress.log(` checking disabled state of "${selector}"`); return this._scheduleRerunnableTask(progress, info.world, task); }, this._page._timeoutSettings.timeout(options)); } - async isEnabled(selector: string, options: types.TimeoutOptions = {}): Promise { - return !(await this.isDisabled(selector, options)); + async isEnabled(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise { + return !(await this.isDisabled(metadata, selector, options)); } - async isEditable(selector: string, options: types.TimeoutOptions = {}): Promise { + async isEditable(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise { + const controller = new ProgressController(metadata, this); const info = this._page.selectors._parseSelector(selector); const task = dom.editableTask(info); - return runAbortableTask(async progress => { + return controller.run(async progress => { progress.log(` checking editable state of "${selector}"`); return this._scheduleRerunnableTask(progress, info.world, task); }, this._page._timeoutSettings.timeout(options)); } - async isChecked(selector: string, options: types.TimeoutOptions = {}): Promise { + async isChecked(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise { + const controller = new ProgressController(metadata, this); const info = this._page.selectors._parseSelector(selector); const task = dom.checkedTask(info); - return runAbortableTask(async progress => { + return controller.run(async progress => { progress.log(` checking checked state of "${selector}"`); return this._scheduleRerunnableTask(progress, info.world, task); }, this._page._timeoutSettings.timeout(options)); } - async hover(controller: ProgressController, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) { + async hover(metadata: CallMetadata, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._hover(progress, options))); }, this._page._timeoutSettings.timeout(options)); } - async selectOption(controller: ProgressController, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions = {}): Promise { + async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions = {}): Promise { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { return await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._selectOption(progress, elements, values, options)); }, this._page._timeoutSettings.timeout(options)); } - async setInputFiles(controller: ProgressController, selector: string, files: types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise { + async setInputFiles(metadata: CallMetadata, selector: string, files: types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._setInputFiles(progress, files, options))); }, this._page._timeoutSettings.timeout(options)); } - async type(controller: ProgressController, selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { + async type(metadata: CallMetadata, selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._type(progress, text, options))); }, this._page._timeoutSettings.timeout(options)); } - async press(controller: ProgressController, selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { + async press(metadata: CallMetadata, selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._press(progress, key, options))); }, this._page._timeoutSettings.timeout(options)); } - async check(controller: ProgressController, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + async check(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._setChecked(progress, true, options))); }, this._page._timeoutSettings.timeout(options)); } - async uncheck(controller: ProgressController, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + async uncheck(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + const controller = new ProgressController(metadata, this); return controller.run(async progress => { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._setChecked(progress, false, options))); }, this._page._timeoutSettings.timeout(options)); } - async _waitForFunctionExpression(expression: string, isFunction: boolean | undefined, arg: any, options: types.WaitForFunctionOptions = {}): Promise> { + async _waitForFunctionExpression(metadata: CallMetadata, expression: string, isFunction: boolean | undefined, arg: any, options: types.WaitForFunctionOptions = {}): Promise> { + const controller = new ProgressController(metadata, this); if (typeof options.pollingInterval === 'number') assert(options.pollingInterval > 0, 'Cannot poll with non-positive interval: ' + options.pollingInterval); expression = js.normalizeEvaluationExpression(expression, isFunction); @@ -1038,7 +1064,7 @@ export class Frame extends SdkObject { return injectedScript.pollRaf((progress, continuePolling) => predicate(arg) || continuePolling); return injectedScript.pollInterval(polling, (progress, continuePolling) => predicate(arg) || continuePolling); }, { expression, isFunction, polling: options.pollingInterval, arg }); - return runAbortableTask( + return controller.run( progress => this._scheduleRerunnableHandleTask(progress, 'main', task), this._page._timeoutSettings.timeout(options)); } diff --git a/src/server/instrumentation.ts b/src/server/instrumentation.ts new file mode 100644 index 0000000000..a3009aeaeb --- /dev/null +++ b/src/server/instrumentation.ts @@ -0,0 +1,114 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventEmitter } from 'events'; +import type { Browser } from './browser'; +import type { BrowserContext } from './browserContext'; +import type { BrowserType } from './browserType'; +import type { Frame } from './frames'; +import type { Page } from './page'; + +export type Attribution = { + browserType?: BrowserType; + browser?: Browser; + context?: BrowserContext; + page?: Page; + frame?: Frame; +}; + +export type CallMetadata = { + type: string; + method: string; + params: any; + stack: string; +}; + +export class SdkObject extends EventEmitter { + attribution: Attribution; + instrumentation: Instrumentation; + + protected constructor(parent: SdkObject) { + super(); + this.setMaxListeners(0); + this.attribution = { ...parent.attribution }; + this.instrumentation = parent.instrumentation; + } +} + +export type ActionResult = { + logs: string[], + startTime: number, + endTime: number, + error?: Error, +}; + +export interface Instrumentation { + onContextCreated(context: BrowserContext): Promise; + onContextWillDestroy(context: BrowserContext): Promise; + onContextDidDestroy(context: BrowserContext): Promise; + onActionCheckpoint(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise; + onAfterAction(result: ActionResult, sdkObject: SdkObject, metadata: CallMetadata): Promise; +} + +export interface InstrumentationListener { + onContextCreated?(context: BrowserContext): Promise; + onContextWillDestroy?(context: BrowserContext): Promise; + onContextDidDestroy?(context: BrowserContext): Promise; + onActionCheckpoint?(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise; + onAfterAction?(result: ActionResult, sdkObject: SdkObject, metadata: CallMetadata): Promise; +} + +export class InstrumentationMultiplexer implements Instrumentation { + private _listeners: InstrumentationListener[]; + + constructor(listeners: InstrumentationListener[]) { + this._listeners = listeners; + } + + async onContextCreated(context: BrowserContext): Promise { + for (const listener of this._listeners) + await listener.onContextCreated?.(context); + } + + async onContextWillDestroy(context: BrowserContext): Promise { + for (const listener of this._listeners) + await listener.onContextWillDestroy?.(context); + } + + async onContextDidDestroy(context: BrowserContext): Promise { + for (const listener of this._listeners) + await listener.onContextDidDestroy?.(context); + } + + async onActionCheckpoint(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise { + for (const listener of this._listeners) + await listener.onActionCheckpoint?.(name, sdkObject, metadata); + } + + async onAfterAction(result: ActionResult, sdkObject: SdkObject, metadata: CallMetadata): Promise { + for (const listener of this._listeners) + await listener.onAfterAction?.(result, sdkObject, metadata); + } +} + +export function internalCallMetadata(): CallMetadata { + return { + type: 'Internal', + method: '', + params: {}, + stack: '' + }; +} diff --git a/src/server/javascript.ts b/src/server/javascript.ts index c014131de8..b8056a46bd 100644 --- a/src/server/javascript.ts +++ b/src/server/javascript.ts @@ -18,7 +18,7 @@ import * as dom from './dom'; import * as utilityScriptSource from '../generated/utilityScriptSource'; import { serializeAsCallArgument } from './common/utilityScriptSerializers'; import type UtilityScript from './injected/utilityScript'; -import { SdkObject } from './sdkObject'; +import { SdkObject } from './instrumentation'; type ObjectId = string; export type RemoteObject = { diff --git a/src/server/network.ts b/src/server/network.ts index 22038fa866..ba310865a9 100644 --- a/src/server/network.ts +++ b/src/server/network.ts @@ -17,7 +17,7 @@ import * as frames from './frames'; import * as types from './types'; import { assert } from '../utils/utils'; -import { SdkObject } from './sdkObject'; +import { SdkObject } from './instrumentation'; export function filterCookies(cookies: types.NetworkCookie[], urls: string[]): types.NetworkCookie[] { const parsedURLs = urls.map(s => new URL(s)); diff --git a/src/server/page.ts b/src/server/page.ts index 5827bf1d49..cd7d8e15ac 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -27,11 +27,11 @@ import { BrowserContext, Video } from './browserContext'; import { ConsoleMessage } from './console'; import * as accessibility from './accessibility'; import { FileChooser } from './fileChooser'; -import { ProgressController, runAbortableTask } from './progress'; +import { ProgressController } from './progress'; import { assert, isError } from '../utils/utils'; import { debugLogger } from '../utils/debugLogger'; import { Selectors } from './selectors'; -import { SdkObject } from './sdkObject'; +import { CallMetadata, SdkObject } from './instrumentation'; export interface PageDelegate { readonly rawMouse: input.RawMouse; @@ -288,7 +288,7 @@ export class Page extends SdkObject { } _addConsoleMessage(type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text?: string) { - const message = new ConsoleMessage(this, type, text, args, location); + const message = new ConsoleMessage(type, text, args, location); const intercepted = this._frameManager.interceptConsoleMessage(message); if (intercepted || !this.listenerCount(Page.Events.Console)) args.forEach(arg => arg.dispose()); @@ -296,8 +296,8 @@ export class Page extends SdkObject { this.emit(Page.Events.Console, message); } - async reload(controller: ProgressController, options: types.NavigateOptions): Promise { - this.mainFrame().setupNavigationProgressController(controller); + async reload(metadata: CallMetadata, options: types.NavigateOptions): Promise { + const controller = this.mainFrame().setupNavigationProgressController(metadata); const response = await controller.run(async progress => { // Note: waitForNavigation may fail before we get response to reload(), // so we should await it immediately. @@ -311,8 +311,8 @@ export class Page extends SdkObject { return response; } - async goBack(controller: ProgressController, options: types.NavigateOptions): Promise { - this.mainFrame().setupNavigationProgressController(controller); + async goBack(metadata: CallMetadata, options: types.NavigateOptions): Promise { + const controller = this.mainFrame().setupNavigationProgressController(metadata); const response = await controller.run(async progress => { // Note: waitForNavigation may fail before we get response to goBack, // so we should catch it immediately. @@ -333,8 +333,8 @@ export class Page extends SdkObject { return response; } - async goForward(controller: ProgressController, options: types.NavigateOptions): Promise { - this.mainFrame().setupNavigationProgressController(controller); + async goForward(metadata: CallMetadata, options: types.NavigateOptions): Promise { + const controller = this.mainFrame().setupNavigationProgressController(metadata); const response = await controller.run(async progress => { // Note: waitForNavigation may fail before we get response to goForward, // so we should catch it immediately. @@ -421,8 +421,9 @@ export class Page extends SdkObject { route.continue(); } - async screenshot(options: types.ScreenshotOptions = {}): Promise { - return runAbortableTask( + async screenshot(metadata: CallMetadata, options: types.ScreenshotOptions = {}): Promise { + const controller = new ProgressController(metadata, this); + return controller.run( progress => this._screenshotter.screenshotPage(progress, options), this._timeoutSettings.timeout(options)); } diff --git a/src/server/playwright.ts b/src/server/playwright.ts index 6e7a9b1b18..8901d58f26 100644 --- a/src/server/playwright.ts +++ b/src/server/playwright.ts @@ -22,12 +22,12 @@ import { PlaywrightOptions } from './browser'; import { Chromium } from './chromium/chromium'; import { Electron } from './electron/electron'; import { Firefox } from './firefox/firefox'; -import { Selectors } from './selectors'; +import { Selectors, serverSelectors } from './selectors'; import { HarTracer } from './supplements/har/harTracer'; import { InspectorController } from './supplements/inspectorController'; import { WebKit } from './webkit/webkit'; import { Registry } from '../utils/registry'; -import { SdkObject } from './sdkObject'; +import { InstrumentationMultiplexer, SdkObject } from './instrumentation'; export class Playwright extends SdkObject { readonly selectors: Selectors; @@ -39,25 +39,23 @@ export class Playwright extends SdkObject { readonly options: PlaywrightOptions; constructor(isInternal: boolean) { - super(null); - this.selectors = new Selectors(this); + const instrumentation = new InstrumentationMultiplexer(isInternal ? [] : [ + new InspectorController(), + new Tracer(), + new HarTracer() + ]); + super({ attribution: {}, instrumentation } as any); this.options = { isInternal, registry: new Registry(path.join(__dirname, '..', '..')), - - contextListeners: isInternal ? [] : [ - new InspectorController(), - new Tracer(), - new HarTracer() - ], rootSdkObject: this, - selectors: this.selectors }; this.chromium = new Chromium(this.options); this.firefox = new Firefox(this.options); this.webkit = new WebKit(this.options); this.electron = new Electron(this.options); this.android = new Android(new AdbBackend(), this.options); + this.selectors = serverSelectors; } } diff --git a/src/server/progress.ts b/src/server/progress.ts index de717fb172..bf7802abbc 100644 --- a/src/server/progress.ts +++ b/src/server/progress.ts @@ -18,13 +18,7 @@ import { TimeoutError } from '../utils/errors'; import { assert, monotonicTime } from '../utils/utils'; import { rewriteErrorMessage } from '../utils/stackTrace'; import { debugLogger, LogName } from '../utils/debugLogger'; - -export type ProgressResult = { - logs: string[], - startTime: number, - endTime: number, - error?: Error, -}; +import { CallMetadata, Instrumentation, SdkObject } from './instrumentation'; export interface Progress { log(message: string): void; @@ -35,16 +29,6 @@ export interface Progress { checkpoint(name: string): Promise; } -export interface ProgressListener { - onProgressCheckpoint(name: string): Promise; - onProgressDone(result: ProgressResult): Promise; -} - -export async function runAbortableTask(task: (progress: Progress) => Promise, timeout: number): Promise { - const controller = new ProgressController(); - return controller.run(task, timeout); -} - export class ProgressController { // Promise and callback that forcefully abort the progress. // This promise always rejects. @@ -59,21 +43,22 @@ export class ProgressController { private _deadline: number = 0; private _timeout: number = 0; private _logRecording: string[] = []; - private _listener?: ProgressListener; + readonly metadata: CallMetadata; + readonly instrumentation: Instrumentation; + readonly sdkObject: SdkObject; - constructor() { + constructor(metadata: CallMetadata, sdkObject: SdkObject) { + this.metadata = metadata; + this.sdkObject = sdkObject; + this.instrumentation = sdkObject.instrumentation; this._forceAbortPromise = new Promise((resolve, reject) => this._forceAbort = reject); - this._forceAbortPromise.catch(e => null); // Prevent unhandle promsie rejection. + this._forceAbortPromise.catch(e => null); // Prevent unhandled promise rejection. } setLogName(logName: LogName) { this._logName = logName; } - setListener(listener: ProgressListener) { - this._listener = listener; - } - async run(task: (progress: Progress) => Promise, timeout?: number): Promise { if (timeout) { this._timeout = timeout; @@ -102,8 +87,7 @@ export class ProgressController { throw new AbortedError(); }, checkpoint: async (name: string) => { - if (this._listener) - await this._listener.onProgressCheckpoint(name); + await this.instrumentation.onActionCheckpoint(name, this.sdkObject, this.metadata); }, }; @@ -115,27 +99,23 @@ export class ProgressController { const result = await Promise.race([promise, this._forceAbortPromise]); clearTimeout(timer); this._state = 'finished'; - if (this._listener) { - await this._listener.onProgressDone({ - startTime, - endTime: monotonicTime(), - logs: this._logRecording, - }); - } + await this.instrumentation.onAfterAction({ + startTime, + endTime: monotonicTime(), + logs: this._logRecording, + }, this.sdkObject, this.metadata); this._logRecording = []; return result; } catch (e) { clearTimeout(timer); this._state = 'aborted'; await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(cleanup))); - if (this._listener) { - await this._listener.onProgressDone({ - startTime, - endTime: monotonicTime(), - logs: this._logRecording, - error: e, - }); - } + await this.instrumentation.onAfterAction({ + startTime, + endTime: monotonicTime(), + logs: this._logRecording, + error: e, + }, this.sdkObject, this.metadata); rewriteErrorMessage(e, e.message + formatLogRecording(this._logRecording) + diff --git a/src/server/sdkObject.ts b/src/server/sdkObject.ts deleted file mode 100644 index 28d6e44daf..0000000000 --- a/src/server/sdkObject.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { EventEmitter } from 'events'; -import type { Browser } from './browser'; -import type { BrowserContext } from './browserContext'; -import type { BrowserType } from './browserType'; -import type { Frame } from './frames'; -import type { Page } from './page'; - -export type Attribution = { - browserType?: BrowserType; - browser?: Browser; - context?: BrowserContext; - page?: Page; - frame?: Frame; -}; - -export class SdkObject extends EventEmitter { - attribution: Attribution; - constructor(parent: SdkObject | null) { - super(); - this.setMaxListeners(0); - this.attribution = { ...parent?.attribution }; - } -} diff --git a/src/server/selectors.ts b/src/server/selectors.ts index 3b785b6ae8..f241ff00a8 100644 --- a/src/server/selectors.ts +++ b/src/server/selectors.ts @@ -19,7 +19,6 @@ import * as frames from './frames'; import * as js from './javascript'; import * as types from './types'; import { ParsedSelector, parseSelector } from './common/selectorParser'; -import { SdkObject } from './sdkObject'; export type SelectorInfo = { parsed: ParsedSelector, @@ -27,12 +26,11 @@ export type SelectorInfo = { selector: string, }; -export class Selectors extends SdkObject { +export class Selectors { readonly _builtinEngines: Set; readonly _engines: Map; - constructor(parent: SdkObject) { - super(parent); + constructor() { // Note: keep in sync with InjectedScript class. this._builtinEngines = new Set([ 'css', 'css:light', @@ -136,6 +134,4 @@ export class Selectors extends SdkObject { } } -export function serverSelectors(parent: SdkObject) { - return new Selectors(parent); -} +export const serverSelectors = new Selectors(); diff --git a/src/server/supplements/har/harTracer.ts b/src/server/supplements/har/harTracer.ts index 8b0d83396b..de657e3f3f 100644 --- a/src/server/supplements/har/harTracer.ts +++ b/src/server/supplements/har/harTracer.ts @@ -16,15 +16,16 @@ import * as fs from 'fs'; import * as util from 'util'; -import { BrowserContext, ContextListener } from '../../browserContext'; +import { BrowserContext } from '../../browserContext'; import { helper } from '../../helper'; import * as network from '../../network'; import { Page } from '../../page'; +import { InstrumentationListener } from '../../instrumentation'; import * as har from './har'; const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); -export class HarTracer implements ContextListener { +export class HarTracer implements InstrumentationListener { private _contextTracers = new Map(); async onContextCreated(context: BrowserContext): Promise { diff --git a/src/server/supplements/inspectorController.ts b/src/server/supplements/inspectorController.ts index a74244c93d..8a96d5a129 100644 --- a/src/server/supplements/inspectorController.ts +++ b/src/server/supplements/inspectorController.ts @@ -14,11 +14,12 @@ * limitations under the License. */ -import { BrowserContext, ContextListener } from '../browserContext'; +import { BrowserContext } from '../browserContext'; import { isDebugMode } from '../../utils/utils'; import { RecorderSupplement } from './recorderSupplement'; +import { InstrumentationListener } from '../instrumentation'; -export class InspectorController implements ContextListener { +export class InspectorController implements InstrumentationListener { async onContextCreated(context: BrowserContext): Promise { if (isDebugMode()) { RecorderSupplement.getOrCreate(context, { diff --git a/src/server/supplements/recorder/recorderApp.ts b/src/server/supplements/recorder/recorderApp.ts index 6fde54ed43..363a653b16 100644 --- a/src/server/supplements/recorder/recorderApp.ts +++ b/src/server/supplements/recorder/recorderApp.ts @@ -22,6 +22,7 @@ import { Page } from '../../page'; import { ProgressController } from '../../progress'; import { createPlaywright } from '../../playwright'; import { EventEmitter } from 'events'; +import { internalCallMetadata } from '../../instrumentation'; const readFileAsync = util.promisify(fs.readFile); @@ -85,12 +86,13 @@ export class RecorderApp extends EventEmitter { this._page.context().close().catch(e => console.error(e)); }); - await this._page.mainFrame().goto(new ProgressController(), 'https://playwright/index.html'); + const mainFrame = this._page.mainFrame(); + await mainFrame.goto(internalCallMetadata(), 'https://playwright/index.html'); } static async open(): Promise { const recorderPlaywright = createPlaywright(true); - const context = await recorderPlaywright.chromium.launchPersistentContext(undefined, { + const context = await recorderPlaywright.chromium.launchPersistentContext(internalCallMetadata(), undefined, { args: [ '--app=data:text/html,', '--window-size=300,800', @@ -98,7 +100,7 @@ export class RecorderApp extends EventEmitter { noDefaultViewport: true }); - const controller = new ProgressController(); + const controller = new ProgressController(internalCallMetadata(), context._browser); await controller.run(async progress => { await context._browser._defaultContext!._loadDefaultContextAsIs(progress); }); diff --git a/src/server/supplements/recorderSupplement.ts b/src/server/supplements/recorderSupplement.ts index 96abd90bd9..d6e3cf2a94 100644 --- a/src/server/supplements/recorderSupplement.ts +++ b/src/server/supplements/recorderSupplement.ts @@ -25,11 +25,11 @@ import { LanguageGenerator } from './recorder/language'; import { JavaScriptLanguageGenerator } from './recorder/javascript'; import { CSharpLanguageGenerator } from './recorder/csharp'; import { PythonLanguageGenerator } from './recorder/python'; -import { ProgressController } from '../progress'; import * as recorderSource from '../../generated/recorderSource'; import * as consoleApiSource from '../../generated/consoleApiSource'; import { BufferedOutput, FileOutput, FlushingTerminalOutput, OutputMultiplexer, RecorderOutput, TerminalOutput, Writable } from './recorder/outputs'; import { EventData, Mode, RecorderApp } from './recorder/recorderApp'; +import { internalCallMetadata } from '../instrumentation'; type BindingSource = { frame: Frame, page: Page }; @@ -238,30 +238,30 @@ export class RecorderSupplement { private async _performAction(frame: Frame, action: actions.Action) { const page = frame._page; - const controller = new ProgressController(); const actionInContext: ActionInContext = { pageAlias: this._pageAliases.get(page)!, ...describeFrame(frame), action }; this._generator.willPerformAction(actionInContext); + const noCallMetadata = internalCallMetadata(); try { const kActionTimeout = 5000; if (action.name === 'click') { const { options } = toClickOptions(action); - await frame.click(controller, action.selector, { ...options, timeout: kActionTimeout }); + await frame.click(noCallMetadata, action.selector, { ...options, timeout: kActionTimeout }); } if (action.name === 'press') { const modifiers = toModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); - await frame.press(controller, action.selector, shortcut, { timeout: kActionTimeout }); + await frame.press(noCallMetadata, action.selector, shortcut, { timeout: kActionTimeout }); } if (action.name === 'check') - await frame.check(controller, action.selector, { timeout: kActionTimeout }); + await frame.check(noCallMetadata, action.selector, { timeout: kActionTimeout }); if (action.name === 'uncheck') - await frame.uncheck(controller, action.selector, { timeout: kActionTimeout }); + await frame.uncheck(noCallMetadata, action.selector, { timeout: kActionTimeout }); if (action.name === 'select') - await frame.selectOption(controller, action.selector, [], action.options.map(value => ({ value })), { timeout: kActionTimeout }); + await frame.selectOption(noCallMetadata, action.selector, [], action.options.map(value => ({ value })), { timeout: kActionTimeout }); } catch (e) { this._generator.performedActionFailed(actionInContext); return; diff --git a/src/trace/traceTypes.ts b/src/trace/traceTypes.ts index 16fc004b53..47914bee6e 100644 --- a/src/trace/traceTypes.ts +++ b/src/trace/traceTypes.ts @@ -78,15 +78,14 @@ export type ActionTraceEvent = { timestamp: number, type: 'action', contextId: string, - action: string, + objectType: string, + method: string, + params: any, + stack?: string, pageId?: string, - selector?: string, - label?: string, - value?: string, startTime: number, endTime: number, logs?: string[], - stack?: string, error?: string, snapshots?: { name: string, snapshotId: string }[], }; diff --git a/src/trace/tracer.ts b/src/trace/tracer.ts index 69a50717a6..4e68f009a1 100644 --- a/src/trace/tracer.ts +++ b/src/trace/tracer.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ActionListener, ActionMetadata, BrowserContext, ContextListener, Video } from '../server/browserContext'; +import { BrowserContext, Video } from '../server/browserContext'; import type { SnapshotterResource as SnapshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter'; import * as trace from './traceTypes'; import * as path from 'path'; @@ -24,17 +24,17 @@ import { createGuid, getFromENV, mkdirIfNeeded, monotonicTime } from '../utils/u import { Page } from '../server/page'; import { Snapshotter } from './snapshotter'; import { helper, RegisteredListener } from '../server/helper'; -import { ProgressResult } from '../server/progress'; import { Dialog } from '../server/dialog'; import { Frame, NavigationEvent } from '../server/frames'; import { snapshotScript } from './snapshotterInjected'; +import { ActionResult, CallMetadata, InstrumentationListener, SdkObject } from '../server/instrumentation'; const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs)); const fsAccessAsync = util.promisify(fs.access.bind(fs)); const envTrace = getFromENV('PW_TRACE_DIR'); -export class Tracer implements ContextListener { +export class Tracer implements InstrumentationListener { private _contextTracers = new Map(); async onContextCreated(context: BrowserContext): Promise { @@ -56,19 +56,27 @@ export class Tracer implements ContextListener { this._contextTracers.delete(context); } } + + async onActionCheckpoint(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise { + this._contextTracers.get(sdkObject.attribution.context!)?.onActionCheckpoint(name, sdkObject, metadata); + } + + async onAfterAction(result: ActionResult, sdkObject: SdkObject, metadata: CallMetadata): Promise { + this._contextTracers.get(sdkObject.attribution.context!)?.onAfterAction(result, sdkObject, metadata); + } } const pageIdSymbol = Symbol('pageId'); const snapshotsSymbol = Symbol('snapshots'); // TODO: this is a hacky way to pass snapshots between onActionCheckpoint and onAfterAction. -function snapshotsForMetadata(metadata: ActionMetadata): { name: string, snapshotId: string }[] { +function snapshotsForMetadata(metadata: CallMetadata): { name: string, snapshotId: string }[] { if (!(metadata as any)[snapshotsSymbol]) (metadata as any)[snapshotsSymbol] = []; return (metadata as any)[snapshotsSymbol]; } -class ContextTracer implements SnapshotterDelegate, ActionListener { +class ContextTracer implements SnapshotterDelegate { private _context: BrowserContext; private _contextId: string; private _traceStoragePromise: Promise; @@ -102,7 +110,6 @@ class ContextTracer implements SnapshotterDelegate, ActionListener { this._eventListeners = [ helper.addEventListener(context, BrowserContext.Events.Page, this._onPage.bind(this)), ]; - this._context._actionListeners.add(this); } onBlob(blob: SnapshotterBlob): void { @@ -147,24 +154,29 @@ class ContextTracer implements SnapshotterDelegate, ActionListener { return (page as any)[pageIdSymbol]; } - async onActionCheckpoint(name: string, metadata: ActionMetadata): Promise { + async onActionCheckpoint(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise { + if (!sdkObject.attribution.page) + return; const snapshotId = createGuid(); snapshotsForMetadata(metadata).push({ name, snapshotId }); - await this._snapshotter.forceSnapshot(metadata.page, snapshotId); + await this._snapshotter.forceSnapshot(sdkObject.attribution.page, snapshotId); } - async onAfterAction(result: ProgressResult, metadata: ActionMetadata): Promise { + async onAfterAction(result: ActionResult, sdkObject: SdkObject, metadata: CallMetadata): Promise { + if (!sdkObject.attribution.page) + return; const event: trace.ActionTraceEvent = { timestamp: monotonicTime(), type: 'action', contextId: this._contextId, - pageId: this.pageId(metadata.page), - action: metadata.type, - selector: typeof metadata.target === 'string' ? metadata.target : undefined, - value: metadata.value, + pageId: this.pageId(sdkObject.attribution.page), + objectType: metadata.type, + method: metadata.method, + // FIXME: filter out evaluation snippets, binary + params: metadata.params, + stack: metadata.stack, startTime: result.startTime, endTime: result.endTime, - stack: metadata.stack, logs: result.logs.slice(), error: result.error ? result.error.stack : undefined, snapshots: snapshotsForMetadata(metadata), @@ -265,7 +277,6 @@ class ContextTracer implements SnapshotterDelegate, ActionListener { async dispose() { this._disposed = true; - this._context._actionListeners.delete(this); helper.removeEventListeners(this._eventListeners); this._snapshotter.dispose(); const event: trace.ContextDestroyedTraceEvent = { diff --git a/src/web/traceViewer/ui/actionList.tsx b/src/web/traceViewer/ui/actionList.tsx index 17b6fd5a57..00b8d72d29 100644 --- a/src/web/traceViewer/ui/actionList.tsx +++ b/src/web/traceViewer/ui/actionList.tsx @@ -36,6 +36,7 @@ export const ActionList: React.FC = ({ const targetAction = highlightedAction || selectedAction; return
{actions.map(actionEntry => { const { action, actionId, thumbnailUrl } = actionEntry; + const selector = action.params.selector; return
= ({ >
diff --git a/src/web/traceViewer/ui/timeline.tsx b/src/web/traceViewer/ui/timeline.tsx index 5abccf5f63..ea22e0c9b9 100644 --- a/src/web/traceViewer/ui/timeline.tsx +++ b/src/web/traceViewer/ui/timeline.tsx @@ -54,17 +54,17 @@ export const Timeline: React.FunctionComponent<{ const bars: TimelineBar[] = []; for (const page of context.pages) { for (const entry of page.actions) { - let detail = entry.action.selector || ''; - if (entry.action.action === 'goto') - detail = entry.action.value || ''; + let detail = entry.action.params.selector || ''; + if (entry.action.method === 'goto') + detail = entry.action.params.url || ''; bars.push({ entry, leftTime: entry.action.startTime, rightTime: entry.action.endTime, leftPosition: timeToPosition(measure.width, boundaries, entry.action.startTime), rightPosition: timeToPosition(measure.width, boundaries, entry.action.endTime), - label: entry.action.action + ' ' + detail, - type: entry.action.action, + label: entry.action.method + ' ' + detail, + type: entry.action.method, priority: 0, }); if (entry === (highlightedAction || selectedAction)) diff --git a/test/pause.spec.ts b/test/pause.spec.ts index 992617c46d..937810fae3 100644 --- a/test/pause.spec.ts +++ b/test/pause.spec.ts @@ -15,7 +15,7 @@ */ import { folio } from './fixtures'; -import { ProgressController } from '../lib/server/progress'; +import { internalCallMetadata } from '../lib/server/instrumentation'; const extended = folio.extend<{ recorderFrame: () => Promise, @@ -40,7 +40,7 @@ extended.recorderFrame.init(async ({context, toImpl}, runTest) => { extended.recorderClick.init(async ({ recorderFrame }, runTest) => { await runTest(async (selector: string) => { const frame = await recorderFrame(); - frame.click(new ProgressController(), selector, {}); + frame.click(internalCallMetadata(), selector, {}); }); }); diff --git a/test/trace.spec.ts b/test/trace.spec.ts index 50fe81db8b..bde8973036 100644 --- a/test/trace.spec.ts +++ b/test/trace.spec.ts @@ -43,11 +43,11 @@ it('should record trace', (test, { browserName, platform }) => { expect(pageEvent.contextId).toBe(contextId); const pageId = pageEvent.pageId; - const gotoEvent = traceEvents.find(event => event.type === 'action' && event.action === 'goto') as trace.ActionTraceEvent; + const gotoEvent = traceEvents.find(event => event.type === 'action' && event.method === 'goto') as trace.ActionTraceEvent; expect(gotoEvent).toBeTruthy(); expect(gotoEvent.contextId).toBe(contextId); expect(gotoEvent.pageId).toBe(pageId); - expect(gotoEvent.value).toBe(url); + expect(gotoEvent.params.url).toBe(url); const resourceEvent = traceEvents.find(event => event.type === 'resource' && event.url.endsWith('/frames/style.css')) as trace.NetworkResourceTraceEvent; expect(resourceEvent).toBeTruthy(); @@ -59,7 +59,7 @@ it('should record trace', (test, { browserName, platform }) => { expect(resourceEvent.requestHeaders.length).toBeGreaterThan(0); expect(resourceEvent.requestSha1).toBe('none'); - const clickEvent = traceEvents.find(event => event.type === 'action' && event.action === 'click') as trace.ActionTraceEvent; + const clickEvent = traceEvents.find(event => event.type === 'action' && event.method === 'click') as trace.ActionTraceEvent; expect(clickEvent).toBeTruthy(); expect(clickEvent.snapshots.length).toBe(2); const snapshotId = clickEvent.snapshots[0].snapshotId; @@ -93,11 +93,11 @@ it('should record trace with POST', (test, { browserName, platform }) => { expect(pageEvent.contextId).toBe(contextId); const pageId = pageEvent.pageId; - const gotoEvent = traceEvents.find(event => event.type === 'action' && event.action === 'goto') as trace.ActionTraceEvent; + const gotoEvent = traceEvents.find(event => event.type === 'action' && event.method === 'goto') as trace.ActionTraceEvent; expect(gotoEvent).toBeTruthy(); expect(gotoEvent.contextId).toBe(contextId); expect(gotoEvent.pageId).toBe(pageId); - expect(gotoEvent.value).toBe(url); + expect(gotoEvent.params.url).toBe(url); const resourceEvent = traceEvents.find(event => event.type === 'resource' && event.url.endsWith('/file.json')) as trace.NetworkResourceTraceEvent; expect(resourceEvent).toBeTruthy();