chore: move action instrumentation per-context (#3908)

This allows tracing to expose plugin-like api.

This also remove Progress -> ActionMetadata dependency, leaving
Progress a low-level utility.
This commit is contained in:
Dmitry Gozman 2020-09-17 09:32:54 -07:00 committed by GitHub
parent d4d0239a86
commit 01a4060665
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 304 additions and 311 deletions

View file

@ -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<void> {
}
async onAfterAction(result: ActionResult, metadata?: ActionMetadata): Promise<void> {
}
}

View file

@ -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<void> {
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<void> {
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<void> {
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<channels.ElementHandleSelectOptionResult> {
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<void> {
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<void> {
@ -116,10 +110,9 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann
}
async setInputFiles(params: channels.ElementHandleSetInputFilesParams, metadata?: channels.Metadata): Promise<void> {
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<void> {
@ -127,31 +120,27 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann
}
async type(params: channels.ElementHandleTypeParams, metadata?: channels.Metadata): Promise<void> {
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<void> {
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<void> {
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<void> {
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<channels.ElementHandleBoundingBoxResult> {

View file

@ -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<Frame, channels.FrameInitializer> implements channels.FrameChannel {
private _frame: Frame;
@ -54,10 +53,9 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer
}
async goto(params: channels.FrameGotoParams, metadata?: channels.Metadata): Promise<channels.FrameGotoResult> {
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<ResponseDispatcher>(await this._frame.goto(controller, params.url, params)) };
return await runAction(async controller => {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._frame.goto(controller, params.url, params)) };
}, { ...metadata, type: 'goto', value: params.url, page: this._frame._page });
}
async frameElement(): Promise<channels.FrameFrameElementResult> {
@ -102,10 +100,9 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer
}
async setContent(params: channels.FrameSetContentParams, metadata?: channels.Metadata): Promise<void> {
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<channels.FrameAddScriptTagResult> {
@ -117,24 +114,21 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer
}
async click(params: channels.FrameClickParams, metadata?: channels.Metadata): Promise<void> {
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<void> {
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<void> {
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<void> {
@ -160,53 +154,46 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer
}
async hover(params: channels.FrameHoverParams, metadata?: channels.Metadata): Promise<void> {
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<channels.FrameSelectOptionResult> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<channels.FrameWaitForFunctionResult> {

View file

@ -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<Page, channels.PageInitializer> implements channels.PageChannel {
private _page: Page;
@ -97,21 +95,21 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
}
async reload(params: channels.PageReloadParams, metadata?: channels.Metadata): Promise<channels.PageReloadResult> {
const actionMetadata: ActionMetadata = { ...metadata, type: 'reload', page: this._page };
const controller = new ProgressController(this._page._timeoutSettings.navigationTimeout(params), actionMetadata);
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.reload(controller, params)) };
return await runAction(async controller => {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.reload(controller, params)) };
}, { ...metadata, type: 'reload', page: this._page });
}
async goBack(params: channels.PageGoBackParams, metadata?: channels.Metadata): Promise<channels.PageGoBackResult> {
const actionMetadata: ActionMetadata = { ...metadata, type: 'goBack', page: this._page };
const controller = new ProgressController(this._page._timeoutSettings.navigationTimeout(params), actionMetadata);
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goBack(controller, params)) };
return await runAction(async controller => {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goBack(controller, params)) };
}, { ...metadata, type: 'goBack', page: this._page });
}
async goForward(params: channels.PageGoForwardParams, metadata?: channels.Metadata): Promise<channels.PageGoForwardResult> {
const actionMetadata: ActionMetadata = { ...metadata, type: 'goForward', page: this._page };
const controller = new ProgressController(this._page._timeoutSettings.navigationTimeout(params), actionMetadata);
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goForward(controller, params)) };
return await runAction(async controller => {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goForward(controller, params)) };
}, { ...metadata, type: 'goForward', page: this._page });
}
async emulateMedia(params: channels.PageEmulateMediaParams): Promise<void> {

View file

@ -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();

View file

@ -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);

View file

@ -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<void>;
}
export async function runAction<T>(task: (controller: ProgressController) => Promise<T>, metadata: ActionMetadata): Promise<T> {
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<void>;
onContextDestroyed(context: BrowserContext): Promise<void>;
}
export const contextListeners = new Set<ContextListener>();
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<ActionListener>();
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;

View file

@ -66,9 +66,11 @@ export abstract class BrowserType {
async launch(options: types.LaunchOptions = {}): Promise<Browser> {
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!;
}

View file

@ -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);

View file

@ -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<T extends Node = Node> extends js.JSHandle<T> {
return 'done';
}
async hover(progress: Progress, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise<void> {
const result = await this._hover(progress, options);
return assertDone(throwRetargetableDOMError(result));
async hover(controller: ProgressController, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise<void> {
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<void> {
const result = await this._click(progress, options);
return assertDone(throwRetargetableDOMError(result));
async click(controller: ProgressController, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
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<void> {
const result = await this._dblclick(progress, options);
return assertDone(throwRetargetableDOMError(result));
async dblclick(controller: ProgressController, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
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<string[]> {
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<string[]> {
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<string[] | 'error:notconnected'> {
@ -398,9 +406,11 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
});
}
async fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions = {}): Promise<void> {
const result = await this._fill(progress, value, options);
assertDone(throwRetargetableDOMError(result));
async fill(controller: ProgressController, value: string, options: types.NavigatingActionWaitOptions = {}): Promise<void> {
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<T extends Node = Node> extends js.JSHandle<T> {
}, 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<T extends Node = Node> extends js.JSHandle<T> {
return throwFatalDOMError(result);
}
async type(progress: Progress, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<void> {
const result = await this._type(progress, text, options);
return assertDone(throwRetargetableDOMError(result));
async type(controller: ProgressController, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<void> {
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<T extends Node = Node> extends js.JSHandle<T> {
}, 'input');
}
async press(progress: Progress, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<void> {
const result = await this._press(progress, key, options);
return assertDone(throwRetargetableDOMError(result));
async press(controller: ProgressController, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<void> {
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<T extends Node = Node> extends js.JSHandle<T> {
}, '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'> {

View file

@ -119,10 +119,10 @@ export class ElectronApplication extends EventEmitter {
}
private async _waitForEvent(event: string, predicate?: Function): Promise<any> {
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));
}
}

View file

@ -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<network.Response | null> {
@ -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<string[]> {
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<string[]> {
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<void> {
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<void> {
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<R>(expression: string, isFunction: boolean, arg: any, options: types.WaitForFunctionOptions = {}): Promise<js.SmartHandle<R>> {

View file

@ -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<void>;
onContextDestroyed(context: BrowserContext): Promise<void>;
onAfterAction(result: ActionResult, metadata?: ActionMetadata): Promise<void>;
}
export const instrumentingAgents = new Set<InstrumentingAgent>();

View file

@ -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;
}

View file

@ -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<void>;
@ -29,9 +35,9 @@ export interface Progress {
throwIfAborted(): void;
}
export async function runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeout: number, metadata?: ActionMetadata): Promise<T> {
const controller = new ProgressController(timeout, metadata);
return controller.run(task);
export async function runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeout: number): Promise<T> {
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<void>;
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<T>(task: (progress: Progress) => Promise<T>): Promise<T> {
setListener(listener: (result: ProgressResult) => Promise<void>) {
this._listener = listener;
}
async run<T>(task: (progress: Progress) => Promise<T>, timeout?: number): Promise<T> {
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;
}

View file

@ -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<BrowserContext, ContextTracer>();
// 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<void> {
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<void> {
async dispose(): Promise<void> {
this._context._actionListeners.delete(this);
await this._contextTracer.dispose();
}
async onContextDestroyed(context: BrowserContext): Promise<void> {
async onAfterAction(result: ProgressResult, metadata: ActionMetadata): Promise<void> {
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<void> {
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',

View file

@ -161,13 +161,7 @@ defineWorkerFixture('playwright', async ({browserName, parallelIndex, platform},
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();
}
@ -243,18 +237,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) => {

View file

@ -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' });
});