diff --git a/src/debug/debugController.ts b/src/debug/debugController.ts index d85e5edf043d7..293f996635858 100644 --- a/src/debug/debugController.ts +++ b/src/debug/debugController.ts @@ -14,14 +14,17 @@ * limitations under the License. */ -import { BrowserContext } from '../server/browserContext'; +import { BrowserContext, ContextListener, contextListeners } from '../server/browserContext'; import * as frames from '../server/frames'; import { Page } from '../server/page'; -import { ActionMetadata, ActionResult, InstrumentingAgent } from '../server/instrumentation'; import { isDebugMode } from '../utils/utils'; import * as debugScriptSource from '../generated/debugScriptSource'; -export class DebugController implements InstrumentingAgent { +export function installDebugController() { + contextListeners.add(new DebugController()); +} + +class DebugController implements ContextListener { private async ensureInstalledInFrame(frame: frames.Frame) { try { await frame.extendInjectedScript(debugScriptSource.source); @@ -41,7 +44,4 @@ export class DebugController implements InstrumentingAgent { async onContextDestroyed(context: BrowserContext): Promise { } - - async onAfterAction(result: ActionResult, metadata?: ActionMetadata): Promise { - } } diff --git a/src/dispatchers/elementHandlerDispatcher.ts b/src/dispatchers/elementHandlerDispatcher.ts index 376c7b6b5193a..f74d19b29d202 100644 --- a/src/dispatchers/elementHandlerDispatcher.ts +++ b/src/dispatchers/elementHandlerDispatcher.ts @@ -20,8 +20,7 @@ import * as channels from '../protocol/channels'; import { DispatcherScope, lookupNullableDispatcher } from './dispatcher'; import { JSHandleDispatcher, serializeResult, parseArgument } from './jsHandleDispatcher'; import { FrameDispatcher } from './frameDispatcher'; -import { runAbortableTask } from '../server/progress'; -import { ActionMetadata } from '../server/instrumentation'; +import { runAction } from '../server/browserContext'; export function createHandle(scope: DispatcherScope, handle: js.JSHandle): JSHandleDispatcher { return handle.asElement() ? new ElementHandleDispatcher(scope, handle.asElement()!) : new JSHandleDispatcher(scope, handle); @@ -76,39 +75,34 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann } async hover(params: channels.ElementHandleHoverParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'hover', target: this._elementHandle, page: this._elementHandle._page }; - return runAbortableTask(async progress => { - return await this._elementHandle.hover(progress, params); - }, this._elementHandle._page._timeoutSettings.timeout(params), actionMetadata); + return runAction(async controller => { + return await this._elementHandle.hover(controller, params); + }, { ...metadata, type: 'hover', target: this._elementHandle, page: this._elementHandle._page }); } async click(params: channels.ElementHandleClickParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'click', target: this._elementHandle, page: this._elementHandle._page }; - return runAbortableTask(async progress => { - return await this._elementHandle.click(progress, params); - }, this._elementHandle._page._timeoutSettings.timeout(params), actionMetadata); + return runAction(async controller => { + return await this._elementHandle.click(controller, params); + }, { ...metadata, type: 'click', target: this._elementHandle, page: this._elementHandle._page }); } async dblclick(params: channels.ElementHandleDblclickParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'dblclick', target: this._elementHandle, page: this._elementHandle._page }; - return runAbortableTask(async progress => { - return await this._elementHandle.dblclick(progress, params); - }, this._elementHandle._page._timeoutSettings.timeout(params), actionMetadata); + return runAction(async controller => { + return await this._elementHandle.dblclick(controller, params); + }, { ...metadata, type: 'dblclick', target: this._elementHandle, page: this._elementHandle._page }); } async selectOption(params: channels.ElementHandleSelectOptionParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'selectOption', target: this._elementHandle, page: this._elementHandle._page }; - return runAbortableTask(async progress => { + return runAction(async controller => { const elements = (params.elements || []).map(e => (e as ElementHandleDispatcher)._elementHandle); - return { values: await this._elementHandle.selectOption(progress, elements, params.options || [], params) }; - }, this._elementHandle._page._timeoutSettings.timeout(params), actionMetadata); + return { values: await this._elementHandle.selectOption(controller, elements, params.options || [], params) }; + }, { ...metadata, type: 'selectOption', target: this._elementHandle, page: this._elementHandle._page }); } async fill(params: channels.ElementHandleFillParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'fill', value: params.value, target: this._elementHandle, page: this._elementHandle._page }; - return runAbortableTask(async progress => { - return await this._elementHandle.fill(progress, params.value, params); - }, this._elementHandle._page._timeoutSettings.timeout(params), actionMetadata); + 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 selectText(params: channels.ElementHandleSelectTextParams): Promise { @@ -116,10 +110,9 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann } async setInputFiles(params: channels.ElementHandleSetInputFilesParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'setInputFiles', target: this._elementHandle, page: this._elementHandle._page }; - return runAbortableTask(async progress => { - return await this._elementHandle.setInputFiles(progress, params.files, params); - }, this._elementHandle._page._timeoutSettings.timeout(params), actionMetadata); + return runAction(async controller => { + return await this._elementHandle.setInputFiles(controller, params.files, params); + }, { ...metadata, type: 'setInputFiles', target: this._elementHandle, page: this._elementHandle._page }); } async focus(): Promise { @@ -127,31 +120,27 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann } async type(params: channels.ElementHandleTypeParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'type', value: params.text, target: this._elementHandle, page: this._elementHandle._page }; - return runAbortableTask(async progress => { - return await this._elementHandle.type(progress, params.text, params); - }, this._elementHandle._page._timeoutSettings.timeout(params), actionMetadata); + 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 press(params: channels.ElementHandlePressParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'press', value: params.key, target: this._elementHandle, page: this._elementHandle._page }; - return runAbortableTask(async progress => { - return await this._elementHandle.press(progress, params.key, params); - }, this._elementHandle._page._timeoutSettings.timeout(params), actionMetadata); + 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 check(params: channels.ElementHandleCheckParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'check', target: this._elementHandle, page: this._elementHandle._page }; - return runAbortableTask(async progress => { - return await this._elementHandle.check(progress, params); - }, this._elementHandle._page._timeoutSettings.timeout(params), actionMetadata); + return runAction(async controller => { + return await this._elementHandle.check(controller, params); + }, { ...metadata, type: 'check', target: this._elementHandle, page: this._elementHandle._page }); } async uncheck(params: channels.ElementHandleUncheckParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'uncheck', target: this._elementHandle, page: this._elementHandle._page }; - return runAbortableTask(async progress => { - return await this._elementHandle.uncheck(progress, params); - }, this._elementHandle._page._timeoutSettings.timeout(params), actionMetadata); + return runAction(async controller => { + return await this._elementHandle.uncheck(controller, params); + }, { ...metadata, type: 'uncheck', target: this._elementHandle, page: this._elementHandle._page }); } async boundingBox(): Promise { diff --git a/src/dispatchers/frameDispatcher.ts b/src/dispatchers/frameDispatcher.ts index 301f6495d4c10..b2bcbb809bfd9 100644 --- a/src/dispatchers/frameDispatcher.ts +++ b/src/dispatchers/frameDispatcher.ts @@ -20,8 +20,7 @@ import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatch import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher'; import { parseArgument, serializeResult } from './jsHandleDispatcher'; import { ResponseDispatcher, RequestDispatcher } from './networkDispatchers'; -import { ActionMetadata } from '../server/instrumentation'; -import { ProgressController, runAbortableTask } from '../server/progress'; +import { runAction } from '../server/browserContext'; export class FrameDispatcher extends Dispatcher implements channels.FrameChannel { private _frame: Frame; @@ -54,10 +53,9 @@ export class FrameDispatcher extends Dispatcher { - const page = this._frame._page; - const actionMetadata: ActionMetadata = { ...metadata, type: 'goto', value: params.url, page }; - const controller = new ProgressController(page._timeoutSettings.navigationTimeout(params), actionMetadata); - return { response: lookupNullableDispatcher(await this._frame.goto(controller, params.url, params)) }; + 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 frameElement(): Promise { @@ -102,10 +100,9 @@ export class FrameDispatcher extends Dispatcher { - const page = this._frame._page; - const actionMetadata: ActionMetadata = { ...metadata, type: 'setContent', value: params.html, page }; - const controller = new ProgressController(page._timeoutSettings.navigationTimeout(params), actionMetadata); - return await this._frame.setContent(controller, params.html, params); + 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 addScriptTag(params: channels.FrameAddScriptTagParams): Promise { @@ -117,24 +114,21 @@ export class FrameDispatcher extends Dispatcher { - const actionMetadata: ActionMetadata = { ...metadata, type: 'click', target: params.selector, page: this._frame._page }; - return runAbortableTask(async progress => { - return await this._frame.click(progress, params.selector, params); - }, this._frame._page._timeoutSettings.timeout(params), actionMetadata); + return runAction(async controller => { + return await this._frame.click(controller, params.selector, params); + }, { ...metadata, type: 'click', target: params.selector, page: this._frame._page }); } async dblclick(params: channels.FrameDblclickParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'dblclick', target: params.selector, page: this._frame._page }; - return runAbortableTask(async progress => { - return await this._frame.dblclick(progress, params.selector, params); - }, this._frame._page._timeoutSettings.timeout(params), actionMetadata); + return runAction(async controller => { + return await this._frame.dblclick(controller, params.selector, params); + }, { ...metadata, type: 'dblclick', target: params.selector, page: this._frame._page }); } async fill(params: channels.FrameFillParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'fill', value: params.value, target: params.selector, page: this._frame._page }; - return runAbortableTask(async progress => { - return await this._frame.fill(progress, params.selector, params.value, params); - }, this._frame._page._timeoutSettings.timeout(params), actionMetadata); + 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 focus(params: channels.FrameFocusParams): Promise { @@ -160,53 +154,46 @@ export class FrameDispatcher extends Dispatcher { - const actionMetadata: ActionMetadata = { ...metadata, type: 'hover', target: params.selector, page: this._frame._page }; - return runAbortableTask(async progress => { - return await this._frame.hover(progress, params.selector, params); - }, this._frame._page._timeoutSettings.timeout(params), actionMetadata); + return runAction(async controller => { + return await this._frame.hover(controller, params.selector, params); + }, { ...metadata, type: 'hover', target: params.selector, page: this._frame._page }); } async selectOption(params: channels.FrameSelectOptionParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'selectOption', target: params.selector, page: this._frame._page }; - return runAbortableTask(async progress => { + return runAction(async controller => { const elements = (params.elements || []).map(e => (e as ElementHandleDispatcher)._elementHandle); - return { values: await this._frame.selectOption(progress, params.selector, elements, params.options || [], params) }; - }, this._frame._page._timeoutSettings.timeout(params), actionMetadata); + return { values: await this._frame.selectOption(controller, params.selector, elements, params.options || [], params) }; + }, { ...metadata, type: 'selectOption', target: params.selector, page: this._frame._page }); } async setInputFiles(params: channels.FrameSetInputFilesParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'setInputFiles', target: params.selector, page: this._frame._page }; - return runAbortableTask(async progress => { - return await this._frame.setInputFiles(progress, params.selector, params.files, params); - }, this._frame._page._timeoutSettings.timeout(params), actionMetadata); + 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 type(params: channels.FrameTypeParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'type', value: params.text, target: params.selector, page: this._frame._page }; - return runAbortableTask(async progress => { - return await this._frame.type(progress, params.selector, params.text, params); - }, this._frame._page._timeoutSettings.timeout(params), actionMetadata); + 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 press(params: channels.FramePressParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'press', value: params.key, target: params.selector, page: this._frame._page }; - return runAbortableTask(async progress => { - return await this._frame.press(progress, params.selector, params.key, params); - }, this._frame._page._timeoutSettings.timeout(params), actionMetadata); + 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 check(params: channels.FrameCheckParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'check', target: params.selector, page: this._frame._page }; - return runAbortableTask(async progress => { - return await this._frame.check(progress, params.selector, params); - }, this._frame._page._timeoutSettings.timeout(params), actionMetadata); + return runAction(async controller => { + return await this._frame.check(controller, params.selector, params); + }, { ...metadata, type: 'check', target: params.selector, page: this._frame._page }); } async uncheck(params: channels.FrameUncheckParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'uncheck', target: params.selector, page: this._frame._page }; - return runAbortableTask(async progress => { - return await this._frame.uncheck(progress, params.selector, params); - }, this._frame._page._timeoutSettings.timeout(params), actionMetadata); + return runAction(async controller => { + return await this._frame.uncheck(controller, params.selector, params); + }, { ...metadata, type: 'uncheck', target: params.selector, page: this._frame._page }); } async waitForFunction(params: channels.FrameWaitForFunctionParams): Promise { diff --git a/src/dispatchers/pageDispatcher.ts b/src/dispatchers/pageDispatcher.ts index 99b2e6469a939..de7bf1bf4a776 100644 --- a/src/dispatchers/pageDispatcher.ts +++ b/src/dispatchers/pageDispatcher.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { BrowserContext } from '../server/browserContext'; +import { BrowserContext, runAction } from '../server/browserContext'; import { Frame } from '../server/frames'; import { Request } from '../server/network'; import { Page, Worker } from '../server/page'; @@ -31,8 +31,6 @@ import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatche import { FileChooser } from '../server/fileChooser'; import { CRCoverage } from '../server/chromium/crCoverage'; import { VideoDispatcher } from './videoDispatcher'; -import { ActionMetadata } from '../server/instrumentation'; -import { ProgressController } from '../server/progress'; export class PageDispatcher extends Dispatcher implements channels.PageChannel { private _page: Page; @@ -97,21 +95,21 @@ export class PageDispatcher extends Dispatcher i } async reload(params: channels.PageReloadParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'reload', page: this._page }; - const controller = new ProgressController(this._page._timeoutSettings.navigationTimeout(params), actionMetadata); - return { response: lookupNullableDispatcher(await this._page.reload(controller, params)) }; + return await runAction(async controller => { + return { response: lookupNullableDispatcher(await this._page.reload(controller, params)) }; + }, { ...metadata, type: 'reload', page: this._page }); } async goBack(params: channels.PageGoBackParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'goBack', page: this._page }; - const controller = new ProgressController(this._page._timeoutSettings.navigationTimeout(params), actionMetadata); - return { response: lookupNullableDispatcher(await this._page.goBack(controller, params)) }; + return await runAction(async controller => { + return { response: lookupNullableDispatcher(await this._page.goBack(controller, params)) }; + }, { ...metadata, type: 'goBack', page: this._page }); } async goForward(params: channels.PageGoForwardParams, metadata?: channels.Metadata): Promise { - const actionMetadata: ActionMetadata = { ...metadata, type: 'goForward', page: this._page }; - const controller = new ProgressController(this._page._timeoutSettings.navigationTimeout(params), actionMetadata); - return { response: lookupNullableDispatcher(await this._page.goForward(controller, params)) }; + return await runAction(async controller => { + return { response: lookupNullableDispatcher(await this._page.goForward(controller, params)) }; + }, { ...metadata, type: 'goForward', page: this._page }); } async emulateMedia(params: channels.PageEmulateMediaParams): Promise { diff --git a/src/inprocess.ts b/src/inprocess.ts index 5c913850f49e8..ffff5271ca2a3 100644 --- a/src/inprocess.ts +++ b/src/inprocess.ts @@ -20,11 +20,10 @@ import type { Playwright as PlaywrightAPI } from './client/playwright'; import { PlaywrightDispatcher } from './dispatchers/playwrightDispatcher'; import { Connection } from './client/connection'; import { BrowserServerLauncherImpl } from './browserServerImpl'; -import { instrumentingAgents } from './server/instrumentation'; -import { DebugController } from './debug/debugController'; +import { installDebugController } from './debug/debugController'; export function setupInProcess(playwright: PlaywrightImpl): PlaywrightAPI { - instrumentingAgents.add(new DebugController()); + installDebugController(); const clientConnection = new Connection(); const dispatcherConnection = new DispatcherConnection(); diff --git a/src/server.ts b/src/server.ts index 422a58e5ad63c..163e01ce9445b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -20,10 +20,9 @@ import { Playwright } from './server/playwright'; import { PlaywrightDispatcher } from './dispatchers/playwrightDispatcher'; import { Electron } from './server/electron/electron'; import { gracefullyCloseAll } from './server/processLauncher'; -import { instrumentingAgents } from './server/instrumentation'; -import { DebugController } from './debug/debugController'; +import { installDebugController } from './debug/debugController'; -instrumentingAgents.add(new DebugController()); +installDebugController(); const dispatcherConnection = new DispatcherConnection(); const transport = new Transport(process.stdout, process.stdin); diff --git a/src/server/browserContext.ts b/src/server/browserContext.ts index 9e20d01c41271..8478285702478 100644 --- a/src/server/browserContext.ts +++ b/src/server/browserContext.ts @@ -18,13 +18,13 @@ import { EventEmitter } from 'events'; import { TimeoutSettings } from '../utils/timeoutSettings'; import { Browser } from './browser'; +import * as dom from './dom'; import { Download } from './download'; import * as frames from './frames'; import { helper } from './helper'; -import { instrumentingAgents } from './instrumentation'; import * as network from './network'; import { Page, PageBinding } from './page'; -import { Progress } from './progress'; +import { Progress, ProgressController, ProgressResult } from './progress'; import { Selectors, serverSelectors } from './selectors'; import * as types from './types'; @@ -43,6 +43,35 @@ export class Video { } } +export type ActionMetadata = { + type: 'click' | 'fill' | 'dblclick' | 'hover' | 'selectOption' | 'setInputFiles' | 'type' | 'press' | 'check' | 'uncheck' | 'goto' | 'setContent' | 'goBack' | 'goForward' | 'reload', + page: Page, + target?: dom.ElementHandle | string, + value?: string, + stack?: string, +}; + +export interface ActionListener { + onAfterAction(result: ProgressResult, metadata: ActionMetadata): Promise; +} + +export async function runAction(task: (controller: ProgressController) => Promise, metadata: ActionMetadata): Promise { + const controller = new ProgressController(); + controller.setListener(async result => { + 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; + onContextDestroyed(context: BrowserContext): Promise; +} + +export const contextListeners = new Set(); + export abstract class BrowserContext extends EventEmitter { static Events = { Close: 'close', @@ -62,6 +91,7 @@ export abstract class BrowserContext extends EventEmitter { readonly _browser: Browser; readonly _browserContextId: string | undefined; private _selectors?: Selectors; + readonly _actionListeners = new Set(); constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) { super(); @@ -81,8 +111,8 @@ export abstract class BrowserContext extends EventEmitter { } async _initialize() { - for (const agent of instrumentingAgents) - await agent.onContextCreated(this); + for (const listener of contextListeners) + await listener.onContextCreated(this); } _browserClosed() { @@ -226,8 +256,8 @@ export abstract class BrowserContext extends EventEmitter { this._closedStatus = 'closing'; await this._doClose(); await Promise.all([...this._downloads].map(d => d.delete())); - for (const agent of instrumentingAgents) - await agent.onContextDestroyed(this); + for (const listener of contextListeners) + await listener.onContextDestroyed(this); this._didCloseInternal(); } await this._closePromise; diff --git a/src/server/browserType.ts b/src/server/browserType.ts index b353cdb7da2b0..eaf04d8bff771 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -66,9 +66,11 @@ export abstract class BrowserType { async launch(options: types.LaunchOptions = {}): Promise { options = validateLaunchOptions(options); - const controller = new ProgressController(TimeoutSettings.timeout(options)); + const controller = new ProgressController(); controller.setLogName('browser'); - const browser = await controller.run(progress => this._innerLaunch(progress, options, undefined).catch(e => { throw this._rewriteStartupError(e); })); + const browser = await controller.run(progress => { + return this._innerLaunch(progress, options, undefined).catch(e => { throw this._rewriteStartupError(e); }); + }, TimeoutSettings.timeout(options)); return browser; } @@ -76,9 +78,11 @@ export abstract class BrowserType { options = validateLaunchOptions(options); const persistent: types.BrowserContextOptions = options; validateBrowserContextOptions(persistent); - const controller = new ProgressController(TimeoutSettings.timeout(options)); + const controller = new ProgressController(); controller.setLogName('browser'); - const browser = await controller.run(progress => this._innerLaunch(progress, options, persistent, userDataDir).catch(e => { throw this._rewriteStartupError(e); })); + const browser = await controller.run(progress => { + return this._innerLaunch(progress, options, persistent, userDataDir).catch(e => { throw this._rewriteStartupError(e); }); + }, TimeoutSettings.timeout(options)); return browser._defaultContext!; } diff --git a/src/server/chromium/videoRecorder.ts b/src/server/chromium/videoRecorder.ts index 8c81c761eb154..b6c4f7e53cbf9 100644 --- a/src/server/chromium/videoRecorder.ts +++ b/src/server/chromium/videoRecorder.ts @@ -37,7 +37,7 @@ export class VideoRecorder { if (!options.outputFile.endsWith('.webm')) throw new Error('File must have .webm extension'); - const controller = new ProgressController(0); + const controller = new ProgressController(); controller.setLogName('browser'); return await controller.run(async progress => { const recorder = new VideoRecorder(progress); diff --git a/src/server/dom.ts b/src/server/dom.ts index b025e8500f35c..2ccb6f71a722b 100644 --- a/src/server/dom.ts +++ b/src/server/dom.ts @@ -22,7 +22,7 @@ import * as js from './javascript'; import { Page } from './page'; import { SelectorInfo } from './selectors'; import * as types from './types'; -import { Progress, runAbortableTask } from './progress'; +import { Progress, ProgressController, runAbortableTask } from './progress'; import { FatalDOMError, RetargetableDOMError } from './common/domErrors'; export class FrameExecutionContext extends js.ExecutionContext { @@ -356,36 +356,44 @@ export class ElementHandle extends js.JSHandle { return 'done'; } - async hover(progress: Progress, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise { - const result = await this._hover(progress, options); - return assertDone(throwRetargetableDOMError(result)); + async hover(controller: ProgressController, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise { + return controller.run(async progress => { + const result = await this._hover(progress, options); + return assertDone(throwRetargetableDOMError(result)); + }, this._page._timeoutSettings.timeout(options)); } _hover(progress: Progress, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise<'error:notconnected' | 'done'> { return this._retryPointerAction(progress, 'hover', false /* waitForEnabled */, point => this._page.mouse.move(point.x, point.y), options); } - async click(progress: Progress, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise { - const result = await this._click(progress, options); - return assertDone(throwRetargetableDOMError(result)); + async click(controller: ProgressController, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise { + return controller.run(async progress => { + const result = await this._click(progress, options); + return assertDone(throwRetargetableDOMError(result)); + }, this._page._timeoutSettings.timeout(options)); } _click(progress: Progress, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { return this._retryPointerAction(progress, 'click', true /* waitForEnabled */, point => this._page.mouse.click(point.x, point.y, options), options); } - async dblclick(progress: Progress, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise { - const result = await this._dblclick(progress, options); - return assertDone(throwRetargetableDOMError(result)); + async dblclick(controller: ProgressController, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise { + return controller.run(async progress => { + const result = await this._dblclick(progress, options); + return assertDone(throwRetargetableDOMError(result)); + }, this._page._timeoutSettings.timeout(options)); } _dblclick(progress: Progress, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { return this._retryPointerAction(progress, 'dblclick', true /* waitForEnabled */, point => this._page.mouse.dblclick(point.x, point.y, options), options); } - async selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions): Promise { - const result = await this._selectOption(progress, elements, values, options); - return throwRetargetableDOMError(result); + async selectOption(controller: ProgressController, elements: ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions): Promise { + return controller.run(async progress => { + const result = await this._selectOption(progress, elements, values, options); + return throwRetargetableDOMError(result); + }, this._page._timeoutSettings.timeout(options)); } async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions): Promise { @@ -398,9 +406,11 @@ export class ElementHandle extends js.JSHandle { }); } - async fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions = {}): Promise { - const result = await this._fill(progress, value, options); - assertDone(throwRetargetableDOMError(result)); + async fill(controller: ProgressController, value: string, options: types.NavigatingActionWaitOptions = {}): Promise { + return controller.run(async progress => { + const result = await this._fill(progress, value, options); + assertDone(throwRetargetableDOMError(result)); + }, this._page._timeoutSettings.timeout(options)); } async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { @@ -441,9 +451,11 @@ export class ElementHandle extends js.JSHandle { }, this._page._timeoutSettings.timeout(options)); } - async setInputFiles(progress: Progress, files: types.FilePayload[], options: types.NavigatingActionWaitOptions) { - const result = await this._setInputFiles(progress, files, options); - return assertDone(throwRetargetableDOMError(result)); + async setInputFiles(controller: ProgressController, files: types.FilePayload[], options: types.NavigatingActionWaitOptions) { + return controller.run(async progress => { + const result = await this._setInputFiles(progress, files, options); + return assertDone(throwRetargetableDOMError(result)); + }, this._page._timeoutSettings.timeout(options)); } async _setInputFiles(progress: Progress, files: types.FilePayload[], options: types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { @@ -480,9 +492,11 @@ export class ElementHandle extends js.JSHandle { return throwFatalDOMError(result); } - async type(progress: Progress, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise { - const result = await this._type(progress, text, options); - return assertDone(throwRetargetableDOMError(result)); + async type(controller: ProgressController, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise { + return controller.run(async progress => { + const result = await this._type(progress, text, options); + return assertDone(throwRetargetableDOMError(result)); + }, this._page._timeoutSettings.timeout(options)); } async _type(progress: Progress, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { @@ -497,9 +511,11 @@ export class ElementHandle extends js.JSHandle { }, 'input'); } - async press(progress: Progress, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise { - const result = await this._press(progress, key, options); - return assertDone(throwRetargetableDOMError(result)); + async press(controller: ProgressController, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise { + return controller.run(async progress => { + const result = await this._press(progress, key, options); + return assertDone(throwRetargetableDOMError(result)); + }, this._page._timeoutSettings.timeout(options)); } async _press(progress: Progress, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { @@ -514,14 +530,18 @@ export class ElementHandle extends js.JSHandle { }, 'input'); } - async check(progress: Progress, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { - const result = await this._setChecked(progress, true, options); - return assertDone(throwRetargetableDOMError(result)); + async check(controller: ProgressController, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + return controller.run(async progress => { + const result = await this._setChecked(progress, true, options); + return assertDone(throwRetargetableDOMError(result)); + }, this._page._timeoutSettings.timeout(options)); } - async uncheck(progress: Progress, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { - const result = await this._setChecked(progress, false, options); - return assertDone(throwRetargetableDOMError(result)); + async uncheck(controller: ProgressController, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + return controller.run(async progress => { + const result = await this._setChecked(progress, false, options); + return assertDone(throwRetargetableDOMError(result)); + }, this._page._timeoutSettings.timeout(options)); } async _setChecked(progress: Progress, state: boolean, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { diff --git a/src/server/electron/electron.ts b/src/server/electron/electron.ts index aea3e55088389..3b00333a0d7c1 100644 --- a/src/server/electron/electron.ts +++ b/src/server/electron/electron.ts @@ -119,10 +119,10 @@ export class ElectronApplication extends EventEmitter { } private async _waitForEvent(event: string, predicate?: Function): Promise { - const progressController = new ProgressController(this._timeoutSettings.timeout({})); + const progressController = new ProgressController(); if (event !== ElectronApplication.Events.Close) this._browserContext._closePromise.then(error => progressController.abort(error)); - return progressController.run(progress => helper.waitForEvent(progress, this, event, predicate).promise); + return progressController.run(progress => helper.waitForEvent(progress, this, event, predicate).promise, this._timeoutSettings.timeout({})); } async _init() { @@ -147,7 +147,7 @@ export class Electron { handleSIGTERM = true, handleSIGHUP = true, } = options; - const controller = new ProgressController(TimeoutSettings.timeout(options)); + const controller = new ProgressController(); controller.setLogName('browser'); return controller.run(async progress => { let app: ElectronApplication | undefined = undefined; @@ -190,6 +190,6 @@ export class Electron { app = new ElectronApplication(browser, nodeConnection); await app._init(); return app; - }); + }, TimeoutSettings.timeout(options)); } } diff --git a/src/server/frames.ts b/src/server/frames.ts index 89fa4493f941c..0c57488376d84 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -475,7 +475,7 @@ export class Frame extends EventEmitter { const response = request ? request._finalRequest().response() : null; await this._page._doSlowMo(); return response; - }); + }, this._page._timeoutSettings.navigationTimeout(options)); } async _waitForNavigation(progress: Progress, options: types.NavigateOptions): Promise { @@ -631,7 +631,7 @@ export class Frame extends EventEmitter { }, { html, tag }); await Promise.all([contentPromise, lifecyclePromise]); await this._page._doSlowMo(); - }); + }, this._page._timeoutSettings.navigationTimeout(options)); } name(): string { @@ -808,16 +808,22 @@ export class Frame extends EventEmitter { }, this._page._timeoutSettings.timeout(options)); } - async click(progress: Progress, selector: string, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._click(progress, options))); + async click(controller: ProgressController, selector: string, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + 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(progress: Progress, selector: string, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._dblclick(progress, options))); + async dblclick(controller: ProgressController, selector: string, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + return controller.run(async progress => { + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._dblclick(progress, options))); + }, this._page._timeoutSettings.timeout(options)); } - async fill(progress: Progress, selector: string, value: string, options: types.NavigatingActionWaitOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._fill(progress, value, options))); + async fill(controller: ProgressController, selector: string, value: string, options: types.NavigatingActionWaitOptions) { + 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 = {}) { @@ -862,32 +868,46 @@ export class Frame extends EventEmitter { }, this._page._timeoutSettings.timeout(options)); } - async hover(progress: Progress, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._hover(progress, options))); + async hover(controller: ProgressController, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) { + 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(progress: Progress, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions = {}): Promise { - return await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._selectOption(progress, elements, values, options)); + async selectOption(controller: ProgressController, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions = {}): Promise { + 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(progress: Progress, selector: string, files: types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._setInputFiles(progress, files, options))); + async setInputFiles(controller: ProgressController, selector: string, files: types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise { + 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(progress: Progress, selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._type(progress, text, options))); + async type(controller: ProgressController, selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { + 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(progress: Progress, selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._press(progress, key, options))); + async press(controller: ProgressController, selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { + 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(progress: Progress, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._setChecked(progress, true, options))); + async check(controller: ProgressController, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + 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(progress: Progress, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._setChecked(progress, false, options))); + async uncheck(controller: ProgressController, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + 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, arg: any, options: types.WaitForFunctionOptions = {}): Promise> { diff --git a/src/server/instrumentation.ts b/src/server/instrumentation.ts deleted file mode 100644 index fb61ac3f96d9e..0000000000000 --- a/src/server/instrumentation.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * 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 type { BrowserContext } from './browserContext'; -import type { ElementHandle } from './dom'; -import type { Page } from './page'; - -export type ActionMetadata = { - type: 'click' | 'fill' | 'dblclick' | 'hover' | 'selectOption' | 'setInputFiles' | 'type' | 'press' | 'check' | 'uncheck' | 'goto' | 'setContent' | 'goBack' | 'goForward' | 'reload', - page: Page, - target?: ElementHandle | string, - value?: string, - stack?: string, -}; - -export type ActionResult = { - logs: string[], - startTime: number, - endTime: number, - error?: Error, -}; - -export interface InstrumentingAgent { - onContextCreated(context: BrowserContext): Promise; - onContextDestroyed(context: BrowserContext): Promise; - onAfterAction(result: ActionResult, metadata?: ActionMetadata): Promise; -} - -export const instrumentingAgents = new Set(); diff --git a/src/server/page.ts b/src/server/page.ts index 85b79aa39476f..9837e443a6ae8 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -268,7 +268,7 @@ export class Page extends EventEmitter { const waitPromise = this.mainFrame()._waitForNavigation(progress, options); await this._delegate.reload(); return waitPromise; - }); + }, this._timeoutSettings.navigationTimeout(options)); await this._doSlowMo(); return response; } @@ -283,7 +283,7 @@ export class Page extends EventEmitter { return null; } return waitPromise; - }); + }, this._timeoutSettings.navigationTimeout(options)); await this._doSlowMo(); return response; } @@ -298,7 +298,7 @@ export class Page extends EventEmitter { return null; } return waitPromise; - }); + }, this._timeoutSettings.navigationTimeout(options)); await this._doSlowMo(); return response; } diff --git a/src/server/progress.ts b/src/server/progress.ts index ff48369895cab..12cb53cd3178b 100644 --- a/src/server/progress.ts +++ b/src/server/progress.ts @@ -18,7 +18,13 @@ import { TimeoutError } from '../utils/errors'; import { assert, monotonicTime } from '../utils/utils'; import { rewriteErrorMessage } from '../utils/stackTrace'; import { debugLogger, LogName } from '../utils/debugLogger'; -import { ActionResult, instrumentingAgents, ActionMetadata } from './instrumentation'; + +export type ProgressResult = { + logs: string[], + startTime: number, + endTime: number, + error?: Error, +}; export interface Progress { readonly aborted: Promise; @@ -29,9 +35,9 @@ export interface Progress { throwIfAborted(): void; } -export async function runAbortableTask(task: (progress: Progress) => Promise, timeout: number, metadata?: ActionMetadata): Promise { - const controller = new ProgressController(timeout, metadata); - return controller.run(task); +export async function runAbortableTask(task: (progress: Progress) => Promise, timeout: number): Promise { + const controller = new ProgressController(); + return controller.run(task, timeout); } export class ProgressController { @@ -48,18 +54,14 @@ export class ProgressController { // Cleanups to be run only in the case of abort. private _cleanups: (() => any)[] = []; - private _metadata?: ActionMetadata; private _logName: LogName = 'api'; private _state: 'before' | 'running' | 'aborted' | 'finished' = 'before'; - private _deadline: number; - private _timeout: number; + private _deadline: number = 0; + private _timeout: number = 0; private _logRecordring: string[] = []; + private _listener?: (result: ProgressResult) => Promise; - constructor(timeout: number, metadata?: ActionMetadata) { - this._timeout = timeout; - this._deadline = timeout ? monotonicTime() + timeout : 0; - this._metadata = metadata; - + constructor() { this._forceAbortPromise = new Promise((resolve, reject) => this._forceAbort = reject); this._forceAbortPromise.catch(e => null); // Prevent unhandle promsie rejection. this._abortedPromise = new Promise(resolve => this._aborted = resolve); @@ -69,7 +71,16 @@ export class ProgressController { this._logName = logName; } - async run(task: (progress: Progress) => Promise): Promise { + setListener(listener: (result: ProgressResult) => Promise) { + this._listener = listener; + } + + async run(task: (progress: Progress) => Promise, timeout?: number): Promise { + if (timeout) { + this._timeout = timeout; + this._deadline = timeout ? monotonicTime() + timeout : 0; + } + assert(this._state === 'before'); this._state = 'running'; @@ -102,32 +113,32 @@ export class ProgressController { const result = await Promise.race([promise, this._forceAbortPromise]); clearTimeout(timer); this._state = 'finished'; - const actionResult: ActionResult = { - startTime, - endTime: monotonicTime(), - logs: this._logRecordring, - }; - for (const agent of instrumentingAgents) - await agent.onAfterAction(actionResult, this._metadata); + if (this._listener) { + await this._listener({ + startTime, + endTime: monotonicTime(), + logs: this._logRecordring, + }); + } this._logRecordring = []; return result; } catch (e) { this._aborted(); + clearTimeout(timer); + this._state = 'aborted'; + await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(cleanup))); + if (this._listener) { + await this._listener({ + startTime, + endTime: monotonicTime(), + logs: this._logRecordring, + error: e, + }); + } rewriteErrorMessage(e, e.message + formatLogRecording(this._logRecordring) + kLoggingNote); - clearTimeout(timer); - this._state = 'aborted'; - await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(cleanup))); - const actionResult: ActionResult = { - startTime, - endTime: monotonicTime(), - logs: this._logRecordring, - error: e, - }; - for (const agent of instrumentingAgents) - await agent.onAfterAction(actionResult, this._metadata); this._logRecordring = []; throw e; } diff --git a/src/trace/tracer.ts b/src/trace/tracer.ts index a6627f0134493..6bb0b9d1c1136 100644 --- a/src/trace/tracer.ts +++ b/src/trace/tracer.ts @@ -14,70 +14,48 @@ * limitations under the License. */ -import { BrowserContext } from '../server/browserContext'; +import { ActionListener, ActionMetadata, BrowserContext } from '../server/browserContext'; import type { SnapshotterResource as SnapshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter'; import { ContextCreatedTraceEvent, ContextDestroyedTraceEvent, NetworkResourceTraceEvent, ActionTraceEvent, PageCreatedTraceEvent, PageDestroyedTraceEvent } from './traceTypes'; import * as path from 'path'; import * as util from 'util'; import * as fs from 'fs'; import { calculateSha1, createGuid, mkdirIfNeeded, monotonicTime } from '../utils/utils'; -import { ActionResult, InstrumentingAgent, instrumentingAgents, ActionMetadata } from '../server/instrumentation'; import { Page } from '../server/page'; import { Snapshotter } from './snapshotter'; import * as types from '../server/types'; import { ElementHandle } from '../server/dom'; import { helper, RegisteredListener } from '../server/helper'; import { DEFAULT_TIMEOUT } from '../utils/timeoutSettings'; +import { ProgressResult } from '../server/progress'; const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs)); const fsAccessAsync = util.promisify(fs.access.bind(fs)); -export class Tracer implements InstrumentingAgent { - private _contextTracers = new Map(); +// TODO: merge Trace and ContextTracer. +export class Tracer implements ActionListener { + private _context: BrowserContext; + private _contextTracer: ContextTracer; - constructor() { - instrumentingAgents.add(this); - } - - dispose() { - instrumentingAgents.delete(this); - } - - traceContext(context: BrowserContext, traceStorageDir: string, traceFile: string) { - const contextTracer = new ContextTracer(context, traceStorageDir, traceFile); - this._contextTracers.set(context, contextTracer); + constructor(context: BrowserContext, traceStorageDir: string, traceFile: string) { + this._context = context; + this._contextTracer = new ContextTracer(context, traceStorageDir, traceFile); + this._context._actionListeners.add(this); } async captureSnapshot(page: Page, options: types.TimeoutOptions & { label?: string } = {}): Promise { - const contextTracer = this._contextTracers.get(page.context()); - if (contextTracer) - await contextTracer.captureSnapshot(page, options); + await this._contextTracer.captureSnapshot(page, options); } - async onContextCreated(context: BrowserContext): Promise { + async dispose(): Promise { + this._context._actionListeners.delete(this); + await this._contextTracer.dispose(); } - async onContextDestroyed(context: BrowserContext): Promise { + async onAfterAction(result: ProgressResult, metadata: ActionMetadata): Promise { try { - const contextTracer = this._contextTracers.get(context); - if (contextTracer) { - await contextTracer.dispose(); - this._contextTracers.delete(context); - } - } catch (e) { - // Do not throw from instrumentation. - } - } - - async onAfterAction(result: ActionResult, metadata?: ActionMetadata): Promise { - try { - if (!metadata) - return; - const contextTracer = this._contextTracers.get(metadata.page.context()); - if (!contextTracer) - return; - await contextTracer.recordAction(result, metadata); + await this._contextTracer.recordAction(result, metadata); } catch (e) { // Do not throw from instrumentation. } @@ -151,7 +129,7 @@ class ContextTracer implements SnapshotterDelegate { this._appendTraceEvent(event); } - async recordAction(result: ActionResult, metadata: ActionMetadata) { + async recordAction(result: ProgressResult, metadata: ActionMetadata) { const snapshot = await this._takeSnapshot(metadata.page, typeof metadata.target === 'string' ? undefined : metadata.target); const event: ActionTraceEvent = { type: 'action', diff --git a/test/playwright.fixtures.ts b/test/playwright.fixtures.ts index 8dc5434398e02..42d0a7a7be39c 100644 --- a/test/playwright.fixtures.ts +++ b/test/playwright.fixtures.ts @@ -162,13 +162,7 @@ defineWorkerFixture('playwright', async ({browserName, parallelIndex}, test) => await teardownCoverage(); } else { const playwright = require('../index'); - if (options.TRACING) { - const tracerFactory = require('../lib/trace/tracer').Tracer; - playwright.__tracer = new tracerFactory(); - } await test(playwright); - if (playwright.__tracer) - playwright.__tracer.dispose(); await teardownCoverage(); } @@ -227,18 +221,24 @@ defineWorkerFixture('golden', async ({browserName}, test) => { await test(p => path.join(browserName, p)); }); -defineTestFixture('context', async ({browser, playwright, toImpl}, runTest, info) => { +defineTestFixture('context', async ({browser, toImpl}, runTest, info) => { const context = await browser.newContext(); - const { test, config } = info; - if ((playwright as any).__tracer) { + + if (options.TRACING) { + const { test, config } = info; const traceStorageDir = path.join(config.outputDir, 'trace-storage'); const relativePath = path.relative(config.testDir, test.file).replace(/\.spec\.[jt]s/, ''); const sanitizedTitle = test.title.replace(/[^\w\d]+/g, '_'); const traceFile = path.join(config.outputDir, relativePath, sanitizedTitle + '.trace'); - (playwright as any).__tracer.traceContext(toImpl(context), traceStorageDir, traceFile); + const tracerFactory = require('../lib/trace/tracer').Tracer; + (context as any).__tracer = new tracerFactory(toImpl(context), traceStorageDir, traceFile); } + await runTest(context); await context.close(); + + if ((context as any).__tracer) + await (context as any).__tracer.dispose(); }); defineTestFixture('page', async ({context, playwright, toImpl}, runTest, info) => { diff --git a/test/snapshot.spec.ts b/test/snapshot.spec.ts index 5ef6d26fa5961..bc5306746d2c8 100644 --- a/test/snapshot.spec.ts +++ b/test/snapshot.spec.ts @@ -18,7 +18,7 @@ import { it, options } from './playwright.fixtures'; it('should not throw', (test, parameters) => { test.skip(!options.TRACING); -}, async ({page, server, playwright, toImpl}) => { +}, async ({page, server, context, toImpl}) => { await page.goto(server.PREFIX + '/snapshot/snapshot-with-css.html'); - await (playwright as any).__tracer.captureSnapshot(toImpl(page), { label: 'snapshot' }); + await (context as any).__tracer.captureSnapshot(toImpl(page), { label: 'snapshot' }); });