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:
parent
d4d0239a86
commit
01a4060665
|
|
@ -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> {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'> {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>> {
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue