chore: introduce instrumentation api (#5385)

This commit is contained in:
Pavel Feldman 2021-02-09 14:44:48 -08:00 committed by GitHub
parent 1240dd48cb
commit 2e01fbdbec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 609 additions and 613 deletions

View File

@ -31,9 +31,9 @@ import { createGuid } from './utils/utils';
import { SelectorsDispatcher } from './dispatchers/selectorsDispatcher';
import { Selectors } from './server/selectors';
import { BrowserContext, Video } from './server/browserContext';
import { StreamDispatcher, StreamWrapper } from './dispatchers/streamDispatcher';
import { StreamDispatcher } from './dispatchers/streamDispatcher';
import { ProtocolLogger } from './server/types';
import { SdkObject } from './server/sdkObject';
import { CallMetadata, internalCallMetadata, SdkObject } from './server/instrumentation';
export class BrowserServerLauncherImpl implements BrowserServerLauncher {
private _browserType: BrowserType;
@ -43,7 +43,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
}
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServerImpl> {
const browser = await this._browserType.launch({
const browser = await this._browserType.launch(internalCallMetadata(), {
...options,
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
@ -119,7 +119,7 @@ export class BrowserServerImpl extends EventEmitter implements BrowserServer {
connection.dispatch(JSON.parse(Buffer.from(message).toString()));
});
socket.on('error', () => {});
const selectors = new Selectors(this._browser.options.rootSdkObject);
const selectors = new Selectors();
const scope = connection.rootDispatcher();
const remoteBrowser = new RemoteBrowserDispatcher(scope, this._browser, selectors);
socket.on('close', () => {
@ -156,12 +156,12 @@ class ConnectedBrowser extends BrowserDispatcher {
this._selectors = selectors;
}
async newContext(params: channels.BrowserNewContextParams): Promise<{ context: channels.BrowserContextChannel }> {
async newContext(params: channels.BrowserNewContextParams, metadata: CallMetadata): Promise<{ context: channels.BrowserContextChannel }> {
if (params.recordVideo) {
// TODO: we should create a separate temp directory or accept a launchServer parameter.
params.recordVideo.dir = this._object.options.downloadsPath!;
}
const result = await super.newContext(params);
const result = await super.newContext(params, metadata);
const dispatcher = result.context as BrowserContextDispatcher;
dispatcher._object.on(BrowserContext.Events.VideoStarted, (video: Video) => this._sendVideo(dispatcher, video));
dispatcher._object._setSelectors(this._selectors);
@ -189,7 +189,7 @@ class ConnectedBrowser extends BrowserDispatcher {
video._waitForCallbackOnFinish(async () => {
const readable = fs.createReadStream(video._path);
await new Promise(f => readable.on('readable', f));
const stream = new StreamDispatcher(this._remoteBrowser!._scope, new StreamWrapper(this._object, readable));
const stream = new StreamDispatcher(this._remoteBrowser!._scope, readable);
this._remoteBrowser!._dispatchEvent('video', {
stream,
context: contextDispatcher,

View File

@ -74,11 +74,11 @@ export class ScreenshotGenerator {
const snapshots = action.snapshots || [];
const snapshotId = snapshots.length ? snapshots[0].snapshotId : undefined;
const snapshotUrl = this._snapshotServer.snapshotUrl(action.pageId!, snapshotId, action.endTime);
console.log('Generating screenshot for ' + action.action); // eslint-disable-line no-console
console.log('Generating screenshot for ' + action.method); // eslint-disable-line no-console
await page.evaluate(snapshotUrl => (window as any).showSnapshot(snapshotUrl), snapshotUrl);
try {
const element = await page.$(action.selector || '*[__playwright_target__]');
const element = await page.$(action.params.selector || '*[__playwright_target__]');
if (element) {
await element.evaluate(e => {
e.style.backgroundColor = '#ff69b460';

View File

@ -18,6 +18,7 @@ import { Dispatcher, DispatcherScope, existingDispatcher } from './dispatcher';
import { Android, AndroidDevice, SocketBackend } from '../server/android/android';
import * as channels from '../protocol/channels';
import { BrowserContextDispatcher } from './browserContextDispatcher';
import { CallMetadata } from '../server/instrumentation';
export class AndroidDispatcher extends Dispatcher<Android, channels.AndroidInitializer> implements channels.AndroidChannel {
constructor(scope: DispatcherScope, android: Android) {
@ -141,7 +142,7 @@ export class AndroidDeviceDispatcher extends Dispatcher<AndroidDevice, channels.
return { result: (await this._object.shell(params.command)).toString('base64') };
}
async open(params: channels.AndroidDeviceOpenParams, metadata?: channels.Metadata): Promise<channels.AndroidDeviceOpenResult> {
async open(params: channels.AndroidDeviceOpenParams, metadata: CallMetadata): Promise<channels.AndroidDeviceOpenResult> {
const socket = await this._object.open(params.command);
return { socket: new AndroidSocketDispatcher(this._scope, socket) };
}
@ -182,11 +183,11 @@ export class AndroidSocketDispatcher extends Dispatcher<SocketBackend, channels.
});
}
async write(params: channels.AndroidSocketWriteParams, metadata?: channels.Metadata): Promise<void> {
async write(params: channels.AndroidSocketWriteParams, metadata: CallMetadata): Promise<void> {
await this._object.write(Buffer.from(params.data, 'base64'));
}
async close(params: channels.AndroidSocketCloseParams, metadata?: channels.Metadata): Promise<void> {
async close(params: channels.AndroidSocketCloseParams, metadata: CallMetadata): Promise<void> {
await this._object.close();
}
}

View File

@ -22,6 +22,7 @@ import { RouteDispatcher, RequestDispatcher } from './networkDispatchers';
import { CRBrowserContext } from '../server/chromium/crBrowser';
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
import { RecorderSupplement } from '../server/supplements/recorderSupplement';
import { CallMetadata } from '../server/instrumentation';
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextInitializer> implements channels.BrowserContextChannel {
private _context: BrowserContext;
@ -120,8 +121,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
});
}
async storageState(): Promise<channels.BrowserContextStorageStateResult> {
return await this._context.storageState();
async storageState(params: channels.BrowserContextStorageStateParams, metadata: CallMetadata): Promise<channels.BrowserContextStorageStateResult> {
return await this._context.storageState(metadata);
}
async close(): Promise<void> {

View File

@ -21,6 +21,7 @@ import { CDPSessionDispatcher } from './cdpSessionDispatcher';
import { Dispatcher, DispatcherScope } from './dispatcher';
import { CRBrowser } from '../server/chromium/crBrowser';
import { PageDispatcher } from './pageDispatcher';
import { CallMetadata } from '../server/instrumentation';
export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserInitializer> implements channels.BrowserChannel {
constructor(scope: DispatcherScope, browser: Browser) {
@ -33,10 +34,10 @@ export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserIniti
this._dispose();
}
async newContext(params: channels.BrowserNewContextParams): Promise<channels.BrowserNewContextResult> {
async newContext(params: channels.BrowserNewContextParams, metadata: CallMetadata): Promise<channels.BrowserNewContextResult> {
const context = await this._object.newContext(params);
if (params.storageState)
await context.setStorageState(params.storageState);
await context.setStorageState(metadata, params.storageState);
return { context: new BrowserContextDispatcher(this._scope, context) };
}

View File

@ -19,6 +19,7 @@ import { BrowserDispatcher } from './browserDispatcher';
import * as channels from '../protocol/channels';
import { Dispatcher, DispatcherScope } from './dispatcher';
import { BrowserContextDispatcher } from './browserContextDispatcher';
import { CallMetadata } from '../server/instrumentation';
export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.BrowserTypeInitializer> implements channels.BrowserTypeChannel {
constructor(scope: DispatcherScope, browserType: BrowserType) {
@ -28,13 +29,13 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
}, true);
}
async launch(params: channels.BrowserTypeLaunchParams): Promise<channels.BrowserTypeLaunchResult> {
const browser = await this._object.launch(params);
async launch(params: channels.BrowserTypeLaunchParams, metadata: CallMetadata): Promise<channels.BrowserTypeLaunchResult> {
const browser = await this._object.launch(metadata, params);
return { browser: new BrowserDispatcher(this._scope, browser) };
}
async launchPersistentContext(params: channels.BrowserTypeLaunchPersistentContextParams): Promise<channels.BrowserTypeLaunchPersistentContextResult> {
const browserContext = await this._object.launchPersistentContext(params.userDataDir, params);
async launchPersistentContext(params: channels.BrowserTypeLaunchPersistentContextParams, metadata: CallMetadata): Promise<channels.BrowserTypeLaunchPersistentContextResult> {
const browserContext = await this._object.launchPersistentContext(metadata, params.userDataDir, params);
return { context: new BrowserContextDispatcher(this._scope, browserContext) };
}
}

View File

@ -21,7 +21,7 @@ import { createScheme, Validator, ValidationError } from '../protocol/validator'
import { assert, createGuid, debugAssert, isUnderTest } from '../utils/utils';
import { tOptional } from '../protocol/validatorPrimitives';
import { kBrowserOrContextClosedError } from '../utils/errors';
import { SdkObject } from '../server/sdkObject';
import { CallMetadata } from '../server/instrumentation';
export const dispatcherSymbol = Symbol('dispatcher');
@ -39,14 +39,7 @@ export function lookupNullableDispatcher<DispatcherType>(object: any | null): Di
return object ? lookupDispatcher(object) : undefined;
}
export type CallMetadata = channels.Metadata & {
object: SdkObject;
type: string;
method: string;
params: any;
};
export class Dispatcher<Type extends SdkObject, Initializer> extends EventEmitter implements channels.Channel {
export class Dispatcher<Type, Initializer> extends EventEmitter implements channels.Channel {
private _connection: DispatcherConnection;
private _isScope: boolean;
// Parent is always "isScope".
@ -120,9 +113,9 @@ export class Dispatcher<Type extends SdkObject, Initializer> extends EventEmitte
}
export type DispatcherScope = Dispatcher<any, any>;
class Root extends Dispatcher<SdkObject, {}> {
class Root extends Dispatcher<{}, {}> {
constructor(connection: DispatcherConnection) {
super(connection, new SdkObject(null), '', {}, true, '');
super(connection, {}, '', {}, true, '');
}
}
@ -186,8 +179,7 @@ export class DispatcherConnection {
if (typeof (dispatcher as any)[method] !== 'function')
throw new Error(`Mismatching dispatcher: "${dispatcher._type}" does not implement "${method}"`);
const callMetadata: CallMetadata = {
...this._validateMetadata(metadata).stack,
object: dispatcher._object,
...this._validateMetadata(metadata),
type: dispatcher._type,
method,
params,

View File

@ -17,7 +17,7 @@
import { Download } from '../server/download';
import * as channels from '../protocol/channels';
import { Dispatcher, DispatcherScope } from './dispatcher';
import { StreamDispatcher, StreamWrapper } from './streamDispatcher';
import { StreamDispatcher } from './streamDispatcher';
import * as fs from 'fs';
import * as util from 'util';
import { mkdirIfNeeded } from '../utils/utils';
@ -65,7 +65,7 @@ export class DownloadDispatcher extends Dispatcher<Download, channels.DownloadIn
try {
const readable = fs.createReadStream(localPath);
await new Promise(f => readable.on('readable', f));
const stream = new StreamDispatcher(this._scope, new StreamWrapper(this._object, readable));
const stream = new StreamDispatcher(this._scope, readable);
// Resolve with a stream, so that client starts saving the data.
resolve({ stream });
// Block the download until the stream is consumed.
@ -87,7 +87,7 @@ export class DownloadDispatcher extends Dispatcher<Download, channels.DownloadIn
return {};
const readable = fs.createReadStream(fileName);
await new Promise(f => readable.on('readable', f));
return { stream: new StreamDispatcher(this._scope, new StreamWrapper(this._object, readable)) };
return { stream: new StreamDispatcher(this._scope, readable) };
}
async failure(): Promise<channels.DownloadFailureResult> {

View File

@ -20,7 +20,7 @@ import * as channels from '../protocol/channels';
import { DispatcherScope, lookupNullableDispatcher } from './dispatcher';
import { JSHandleDispatcher, serializeResult, parseArgument } from './jsHandleDispatcher';
import { FrameDispatcher } from './frameDispatcher';
import { runAction } from '../server/browserContext';
import { CallMetadata } from '../server/instrumentation';
export function createHandle(scope: DispatcherScope, handle: js.JSHandle): JSHandleDispatcher {
return handle.asElement() ? new ElementHandleDispatcher(scope, handle.asElement()!) : new JSHandleDispatcher(scope, handle);
@ -40,171 +40,149 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann
this._elementHandle = elementHandle;
}
async ownerFrame(): Promise<channels.ElementHandleOwnerFrameResult> {
async ownerFrame(params: channels.ElementHandleOwnerFrameParams, metadata: CallMetadata): Promise<channels.ElementHandleOwnerFrameResult> {
return { frame: lookupNullableDispatcher<FrameDispatcher>(await this._elementHandle.ownerFrame()) };
}
async contentFrame(): Promise<channels.ElementHandleContentFrameResult> {
async contentFrame(params: channels.ElementHandleContentFrameParams, metadata: CallMetadata): Promise<channels.ElementHandleContentFrameResult> {
return { frame: lookupNullableDispatcher<FrameDispatcher>(await this._elementHandle.contentFrame()) };
}
async getAttribute(params: channels.ElementHandleGetAttributeParams): Promise<channels.ElementHandleGetAttributeResult> {
async getAttribute(params: channels.ElementHandleGetAttributeParams, metadata: CallMetadata): Promise<channels.ElementHandleGetAttributeResult> {
const value = await this._elementHandle.getAttribute(params.name);
return { value: value === null ? undefined : value };
}
async textContent(): Promise<channels.ElementHandleTextContentResult> {
async textContent(params: channels.ElementHandleTextContentParams, metadata: CallMetadata): Promise<channels.ElementHandleTextContentResult> {
const value = await this._elementHandle.textContent();
return { value: value === null ? undefined : value };
}
async innerText(): Promise<channels.ElementHandleInnerTextResult> {
async innerText(params: channels.ElementHandleInnerTextParams, metadata: CallMetadata): Promise<channels.ElementHandleInnerTextResult> {
return { value: await this._elementHandle.innerText() };
}
async innerHTML(): Promise<channels.ElementHandleInnerHTMLResult> {
async innerHTML(params: channels.ElementHandleInnerHTMLParams, metadata: CallMetadata): Promise<channels.ElementHandleInnerHTMLResult> {
return { value: await this._elementHandle.innerHTML() };
}
async isChecked(): Promise<channels.ElementHandleIsCheckedResult> {
async isChecked(params: channels.ElementHandleIsCheckedParams, metadata: CallMetadata): Promise<channels.ElementHandleIsCheckedResult> {
return { value: await this._elementHandle.isChecked() };
}
async isDisabled(): Promise<channels.ElementHandleIsDisabledResult> {
async isDisabled(params: channels.ElementHandleIsDisabledParams, metadata: CallMetadata): Promise<channels.ElementHandleIsDisabledResult> {
return { value: await this._elementHandle.isDisabled() };
}
async isEditable(): Promise<channels.ElementHandleIsEditableResult> {
async isEditable(params: channels.ElementHandleIsEditableParams, metadata: CallMetadata): Promise<channels.ElementHandleIsEditableResult> {
return { value: await this._elementHandle.isEditable() };
}
async isEnabled(): Promise<channels.ElementHandleIsEnabledResult> {
async isEnabled(params: channels.ElementHandleIsEnabledParams, metadata: CallMetadata): Promise<channels.ElementHandleIsEnabledResult> {
return { value: await this._elementHandle.isEnabled() };
}
async isHidden(): Promise<channels.ElementHandleIsHiddenResult> {
async isHidden(params: channels.ElementHandleIsHiddenParams, metadata: CallMetadata): Promise<channels.ElementHandleIsHiddenResult> {
return { value: await this._elementHandle.isHidden() };
}
async isVisible(): Promise<channels.ElementHandleIsVisibleResult> {
async isVisible(params: channels.ElementHandleIsVisibleParams, metadata: CallMetadata): Promise<channels.ElementHandleIsVisibleResult> {
return { value: await this._elementHandle.isVisible() };
}
async dispatchEvent(params: channels.ElementHandleDispatchEventParams): Promise<void> {
async dispatchEvent(params: channels.ElementHandleDispatchEventParams, metadata: CallMetadata): Promise<void> {
await this._elementHandle.dispatchEvent(params.type, parseArgument(params.eventInit));
}
async scrollIntoViewIfNeeded(params: channels.ElementHandleScrollIntoViewIfNeededParams): Promise<void> {
await this._elementHandle.scrollIntoViewIfNeeded(params);
async scrollIntoViewIfNeeded(params: channels.ElementHandleScrollIntoViewIfNeededParams, metadata: CallMetadata): Promise<void> {
await this._elementHandle.scrollIntoViewIfNeeded(metadata, params);
}
async hover(params: channels.ElementHandleHoverParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._elementHandle.hover(controller, params);
}, { ...metadata, type: 'hover', target: this._elementHandle, page: this._elementHandle._page });
async hover(params: channels.ElementHandleHoverParams, metadata: CallMetadata): Promise<void> {
return await this._elementHandle.hover(metadata, params);
}
async click(params: channels.ElementHandleClickParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._elementHandle.click(controller, params);
}, { ...metadata, type: 'click', target: this._elementHandle, page: this._elementHandle._page });
async click(params: channels.ElementHandleClickParams, metadata: CallMetadata): Promise<void> {
return await this._elementHandle.click(metadata, params);
}
async dblclick(params: channels.ElementHandleDblclickParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._elementHandle.dblclick(controller, params);
}, { ...metadata, type: 'dblclick', target: this._elementHandle, page: this._elementHandle._page });
async dblclick(params: channels.ElementHandleDblclickParams, metadata: CallMetadata): Promise<void> {
return await this._elementHandle.dblclick(metadata, params);
}
async tap(params: channels.ElementHandleTapParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._elementHandle.tap(controller, params);
}, { ...metadata, type: 'tap', target: this._elementHandle, page: this._elementHandle._page });
async tap(params: channels.ElementHandleTapParams, metadata: CallMetadata): Promise<void> {
return await this._elementHandle.tap(metadata, params);
}
async selectOption(params: channels.ElementHandleSelectOptionParams, metadata?: channels.Metadata): Promise<channels.ElementHandleSelectOptionResult> {
return runAction(async controller => {
const elements = (params.elements || []).map(e => (e as ElementHandleDispatcher)._elementHandle);
return { values: await this._elementHandle.selectOption(controller, elements, params.options || [], params) };
}, { ...metadata, type: 'selectOption', target: this._elementHandle, page: this._elementHandle._page });
async selectOption(params: channels.ElementHandleSelectOptionParams, metadata: CallMetadata): Promise<channels.ElementHandleSelectOptionResult> {
const elements = (params.elements || []).map(e => (e as ElementHandleDispatcher)._elementHandle);
return { values: await this._elementHandle.selectOption(metadata, elements, params.options || [], params) };
}
async fill(params: channels.ElementHandleFillParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._elementHandle.fill(controller, params.value, params);
}, { ...metadata, type: 'fill', value: params.value, target: this._elementHandle, page: this._elementHandle._page });
async fill(params: channels.ElementHandleFillParams, metadata: CallMetadata): Promise<void> {
return await this._elementHandle.fill(metadata, params.value, params);
}
async selectText(params: channels.ElementHandleSelectTextParams): Promise<void> {
await this._elementHandle.selectText(params);
async selectText(params: channels.ElementHandleSelectTextParams, metadata: CallMetadata): Promise<void> {
await this._elementHandle.selectText(metadata, params);
}
async setInputFiles(params: channels.ElementHandleSetInputFilesParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._elementHandle.setInputFiles(controller, params.files, params);
}, { ...metadata, type: 'setInputFiles', target: this._elementHandle, page: this._elementHandle._page });
async setInputFiles(params: channels.ElementHandleSetInputFilesParams, metadata: CallMetadata): Promise<void> {
return await this._elementHandle.setInputFiles(metadata, params.files, params);
}
async focus(): Promise<void> {
await this._elementHandle.focus();
async focus(params: channels.ElementHandleFocusParams, metadata: CallMetadata): Promise<void> {
await this._elementHandle.focus(metadata);
}
async type(params: channels.ElementHandleTypeParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._elementHandle.type(controller, params.text, params);
}, { ...metadata, type: 'type', value: params.text, target: this._elementHandle, page: this._elementHandle._page });
async type(params: channels.ElementHandleTypeParams, metadata: CallMetadata): Promise<void> {
return await this._elementHandle.type(metadata, params.text, params);
}
async press(params: channels.ElementHandlePressParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._elementHandle.press(controller, params.key, params);
}, { ...metadata, type: 'press', value: params.key, target: this._elementHandle, page: this._elementHandle._page });
async press(params: channels.ElementHandlePressParams, metadata: CallMetadata): Promise<void> {
return await this._elementHandle.press(metadata, params.key, params);
}
async check(params: channels.ElementHandleCheckParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._elementHandle.check(controller, params);
}, { ...metadata, type: 'check', target: this._elementHandle, page: this._elementHandle._page });
async check(params: channels.ElementHandleCheckParams, metadata: CallMetadata): Promise<void> {
return await this._elementHandle.check(metadata, params);
}
async uncheck(params: channels.ElementHandleUncheckParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._elementHandle.uncheck(controller, params);
}, { ...metadata, type: 'uncheck', target: this._elementHandle, page: this._elementHandle._page });
async uncheck(params: channels.ElementHandleUncheckParams, metadata: CallMetadata): Promise<void> {
return await this._elementHandle.uncheck(metadata, params);
}
async boundingBox(): Promise<channels.ElementHandleBoundingBoxResult> {
async boundingBox(params: channels.ElementHandleBoundingBoxParams, metadata: CallMetadata): Promise<channels.ElementHandleBoundingBoxResult> {
const value = await this._elementHandle.boundingBox();
return { value: value || undefined };
}
async screenshot(params: channels.ElementHandleScreenshotParams): Promise<channels.ElementHandleScreenshotResult> {
return { binary: (await this._elementHandle.screenshot(params)).toString('base64') };
async screenshot(params: channels.ElementHandleScreenshotParams, metadata: CallMetadata): Promise<channels.ElementHandleScreenshotResult> {
return { binary: (await this._elementHandle.screenshot(metadata, params)).toString('base64') };
}
async querySelector(params: channels.ElementHandleQuerySelectorParams): Promise<channels.ElementHandleQuerySelectorResult> {
async querySelector(params: channels.ElementHandleQuerySelectorParams, metadata: CallMetadata): Promise<channels.ElementHandleQuerySelectorResult> {
const handle = await this._elementHandle.$(params.selector);
return { element: handle ? new ElementHandleDispatcher(this._scope, handle) : undefined };
}
async querySelectorAll(params: channels.ElementHandleQuerySelectorAllParams): Promise<channels.ElementHandleQuerySelectorAllResult> {
async querySelectorAll(params: channels.ElementHandleQuerySelectorAllParams, metadata: CallMetadata): Promise<channels.ElementHandleQuerySelectorAllResult> {
const elements = await this._elementHandle.$$(params.selector);
return { elements: elements.map(e => new ElementHandleDispatcher(this._scope, e)) };
}
async evalOnSelector(params: channels.ElementHandleEvalOnSelectorParams): Promise<channels.ElementHandleEvalOnSelectorResult> {
async evalOnSelector(params: channels.ElementHandleEvalOnSelectorParams, metadata: CallMetadata): Promise<channels.ElementHandleEvalOnSelectorResult> {
return { value: serializeResult(await this._elementHandle._$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
}
async evalOnSelectorAll(params: channels.ElementHandleEvalOnSelectorAllParams): Promise<channels.ElementHandleEvalOnSelectorAllResult> {
async evalOnSelectorAll(params: channels.ElementHandleEvalOnSelectorAllParams, metadata: CallMetadata): Promise<channels.ElementHandleEvalOnSelectorAllResult> {
return { value: serializeResult(await this._elementHandle._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
}
async waitForElementState(params: channels.ElementHandleWaitForElementStateParams): Promise<void> {
await this._elementHandle.waitForElementState(params.state, params);
async waitForElementState(params: channels.ElementHandleWaitForElementStateParams, metadata: CallMetadata): Promise<void> {
await this._elementHandle.waitForElementState(metadata, params.state, params);
}
async waitForSelector(params: channels.ElementHandleWaitForSelectorParams): Promise<channels.ElementHandleWaitForSelectorResult> {
return { element: ElementHandleDispatcher.createNullable(this._scope, await this._elementHandle.waitForSelector(params.selector, params)) };
async waitForSelector(params: channels.ElementHandleWaitForSelectorParams, metadata: CallMetadata): Promise<channels.ElementHandleWaitForSelectorResult> {
return { element: ElementHandleDispatcher.createNullable(this._scope, await this._elementHandle.waitForSelector(metadata, params.selector, params)) };
}
}

View File

@ -20,7 +20,7 @@ import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatch
import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher';
import { parseArgument, serializeResult } from './jsHandleDispatcher';
import { ResponseDispatcher, RequestDispatcher } from './networkDispatchers';
import { runAction } from '../server/browserContext';
import { CallMetadata } from '../server/instrumentation';
export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer> implements channels.FrameChannel {
private _frame: Frame;
@ -52,45 +52,43 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer
});
}
async goto(params: channels.FrameGotoParams, metadata?: channels.Metadata): Promise<channels.FrameGotoResult> {
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 goto(params: channels.FrameGotoParams, metadata: CallMetadata): Promise<channels.FrameGotoResult> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._frame.goto(metadata, params.url, params)) };
}
async frameElement(): Promise<channels.FrameFrameElementResult> {
return { element: new ElementHandleDispatcher(this._scope, await this._frame.frameElement()) };
}
async evaluateExpression(params: channels.FrameEvaluateExpressionParams): Promise<channels.FrameEvaluateExpressionResult> {
async evaluateExpression(params: channels.FrameEvaluateExpressionParams, metadata: CallMetadata): Promise<channels.FrameEvaluateExpressionResult> {
return { value: serializeResult(await this._frame._evaluateExpression(params.expression, params.isFunction, parseArgument(params.arg), params.world)) };
}
async evaluateExpressionHandle(params: channels.FrameEvaluateExpressionHandleParams): Promise<channels.FrameEvaluateExpressionHandleResult> {
async evaluateExpressionHandle(params: channels.FrameEvaluateExpressionHandleParams, metadata: CallMetadata): Promise<channels.FrameEvaluateExpressionHandleResult> {
return { handle: createHandle(this._scope, await this._frame._evaluateExpressionHandle(params.expression, params.isFunction, parseArgument(params.arg), params.world)) };
}
async waitForSelector(params: channels.FrameWaitForSelectorParams): Promise<channels.FrameWaitForSelectorResult> {
return { element: ElementHandleDispatcher.createNullable(this._scope, await this._frame.waitForSelector(params.selector, params)) };
async waitForSelector(params: channels.FrameWaitForSelectorParams, metadata: CallMetadata): Promise<channels.FrameWaitForSelectorResult> {
return { element: ElementHandleDispatcher.createNullable(this._scope, await this._frame.waitForSelector(metadata, params.selector, params)) };
}
async dispatchEvent(params: channels.FrameDispatchEventParams): Promise<void> {
return this._frame.dispatchEvent(params.selector, params.type, parseArgument(params.eventInit), params);
async dispatchEvent(params: channels.FrameDispatchEventParams, metadata: CallMetadata): Promise<void> {
return this._frame.dispatchEvent(metadata, params.selector, params.type, parseArgument(params.eventInit), params);
}
async evalOnSelector(params: channels.FrameEvalOnSelectorParams): Promise<channels.FrameEvalOnSelectorResult> {
async evalOnSelector(params: channels.FrameEvalOnSelectorParams, metadata: CallMetadata): Promise<channels.FrameEvalOnSelectorResult> {
return { value: serializeResult(await this._frame._$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
}
async evalOnSelectorAll(params: channels.FrameEvalOnSelectorAllParams): Promise<channels.FrameEvalOnSelectorAllResult> {
async evalOnSelectorAll(params: channels.FrameEvalOnSelectorAllParams, metadata: CallMetadata): Promise<channels.FrameEvalOnSelectorAllResult> {
return { value: serializeResult(await this._frame._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
}
async querySelector(params: channels.FrameQuerySelectorParams): Promise<channels.FrameQuerySelectorResult> {
async querySelector(params: channels.FrameQuerySelectorParams, metadata: CallMetadata): Promise<channels.FrameQuerySelectorResult> {
return { element: ElementHandleDispatcher.createNullable(this._scope, await this._frame.$(params.selector)) };
}
async querySelectorAll(params: channels.FrameQuerySelectorAllParams): Promise<channels.FrameQuerySelectorAllResult> {
async querySelectorAll(params: channels.FrameQuerySelectorAllParams, metadata: CallMetadata): Promise<channels.FrameQuerySelectorAllResult> {
const elements = await this._frame.$$(params.selector);
return { elements: elements.map(e => new ElementHandleDispatcher(this._scope, e)) };
}
@ -99,138 +97,114 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer
return { value: await this._frame.content() };
}
async setContent(params: channels.FrameSetContentParams, metadata?: channels.Metadata): Promise<void> {
return await runAction(async controller => {
return await this._frame.setContent(controller, params.html, params);
}, { ...metadata, type: 'setContent', value: params.html, page: this._frame._page });
async setContent(params: channels.FrameSetContentParams, metadata: CallMetadata): Promise<void> {
return await this._frame.setContent(metadata, params.html, params);
}
async addScriptTag(params: channels.FrameAddScriptTagParams): Promise<channels.FrameAddScriptTagResult> {
async addScriptTag(params: channels.FrameAddScriptTagParams, metadata: CallMetadata): Promise<channels.FrameAddScriptTagResult> {
return { element: new ElementHandleDispatcher(this._scope, await this._frame.addScriptTag(params)) };
}
async addStyleTag(params: channels.FrameAddStyleTagParams): Promise<channels.FrameAddStyleTagResult> {
async addStyleTag(params: channels.FrameAddStyleTagParams, metadata: CallMetadata): Promise<channels.FrameAddStyleTagResult> {
return { element: new ElementHandleDispatcher(this._scope, await this._frame.addStyleTag(params)) };
}
async click(params: channels.FrameClickParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._frame.click(controller, params.selector, params);
}, { ...metadata, type: 'click', target: params.selector, page: this._frame._page });
async click(params: channels.FrameClickParams, metadata: CallMetadata): Promise<void> {
return await this._frame.click(metadata, params.selector, params);
}
async dblclick(params: channels.FrameDblclickParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._frame.dblclick(controller, params.selector, params);
}, { ...metadata, type: 'dblclick', target: params.selector, page: this._frame._page });
async dblclick(params: channels.FrameDblclickParams, metadata: CallMetadata): Promise<void> {
return await this._frame.dblclick(metadata, params.selector, params);
}
async tap(params: channels.FrameTapParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._frame.tap(controller, params.selector, params);
}, { ...metadata, type: 'tap', target: params.selector, page: this._frame._page });
async tap(params: channels.FrameTapParams, metadata: CallMetadata): Promise<void> {
return await this._frame.tap(metadata, params.selector, params);
}
async fill(params: channels.FrameFillParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._frame.fill(controller, params.selector, params.value, params);
}, { ...metadata, type: 'fill', value: params.value, target: params.selector, page: this._frame._page });
async fill(params: channels.FrameFillParams, metadata: CallMetadata): Promise<void> {
return await this._frame.fill(metadata, params.selector, params.value, params);
}
async focus(params: channels.FrameFocusParams): Promise<void> {
await this._frame.focus(params.selector, params);
async focus(params: channels.FrameFocusParams, metadata: CallMetadata): Promise<void> {
await this._frame.focus(metadata, params.selector, params);
}
async textContent(params: channels.FrameTextContentParams): Promise<channels.FrameTextContentResult> {
const value = await this._frame.textContent(params.selector, params);
async textContent(params: channels.FrameTextContentParams, metadata: CallMetadata): Promise<channels.FrameTextContentResult> {
const value = await this._frame.textContent(metadata, params.selector, params);
return { value: value === null ? undefined : value };
}
async innerText(params: channels.FrameInnerTextParams): Promise<channels.FrameInnerTextResult> {
return { value: await this._frame.innerText(params.selector, params) };
async innerText(params: channels.FrameInnerTextParams, metadata: CallMetadata): Promise<channels.FrameInnerTextResult> {
return { value: await this._frame.innerText(metadata, params.selector, params) };
}
async innerHTML(params: channels.FrameInnerHTMLParams): Promise<channels.FrameInnerHTMLResult> {
return { value: await this._frame.innerHTML(params.selector, params) };
async innerHTML(params: channels.FrameInnerHTMLParams, metadata: CallMetadata): Promise<channels.FrameInnerHTMLResult> {
return { value: await this._frame.innerHTML(metadata, params.selector, params) };
}
async getAttribute(params: channels.FrameGetAttributeParams): Promise<channels.FrameGetAttributeResult> {
const value = await this._frame.getAttribute(params.selector, params.name, params);
async getAttribute(params: channels.FrameGetAttributeParams, metadata: CallMetadata): Promise<channels.FrameGetAttributeResult> {
const value = await this._frame.getAttribute(metadata, params.selector, params.name, params);
return { value: value === null ? undefined : value };
}
async isChecked(params: channels.FrameIsCheckedParams): Promise<channels.FrameIsCheckedResult> {
return { value: await this._frame.isChecked(params.selector, params) };
async isChecked(params: channels.FrameIsCheckedParams, metadata: CallMetadata): Promise<channels.FrameIsCheckedResult> {
return { value: await this._frame.isChecked(metadata, params.selector, params) };
}
async isDisabled(params: channels.FrameIsDisabledParams): Promise<channels.FrameIsDisabledResult> {
return { value: await this._frame.isDisabled(params.selector, params) };
async isDisabled(params: channels.FrameIsDisabledParams, metadata: CallMetadata): Promise<channels.FrameIsDisabledResult> {
return { value: await this._frame.isDisabled(metadata, params.selector, params) };
}
async isEditable(params: channels.FrameIsEditableParams): Promise<channels.FrameIsEditableResult> {
return { value: await this._frame.isEditable(params.selector, params) };
async isEditable(params: channels.FrameIsEditableParams, metadata: CallMetadata): Promise<channels.FrameIsEditableResult> {
return { value: await this._frame.isEditable(metadata, params.selector, params) };
}
async isEnabled(params: channels.FrameIsEnabledParams): Promise<channels.FrameIsEnabledResult> {
return { value: await this._frame.isEnabled(params.selector, params) };
async isEnabled(params: channels.FrameIsEnabledParams, metadata: CallMetadata): Promise<channels.FrameIsEnabledResult> {
return { value: await this._frame.isEnabled(metadata, params.selector, params) };
}
async isHidden(params: channels.FrameIsHiddenParams): Promise<channels.FrameIsHiddenResult> {
return { value: await this._frame.isHidden(params.selector, params) };
async isHidden(params: channels.FrameIsHiddenParams, metadata: CallMetadata): Promise<channels.FrameIsHiddenResult> {
return { value: await this._frame.isHidden(metadata, params.selector, params) };
}
async isVisible(params: channels.FrameIsVisibleParams): Promise<channels.FrameIsVisibleResult> {
return { value: await this._frame.isVisible(params.selector, params) };
async isVisible(params: channels.FrameIsVisibleParams, metadata: CallMetadata): Promise<channels.FrameIsVisibleResult> {
return { value: await this._frame.isVisible(metadata, params.selector, params) };
}
async hover(params: channels.FrameHoverParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._frame.hover(controller, params.selector, params);
}, { ...metadata, type: 'hover', target: params.selector, page: this._frame._page });
async hover(params: channels.FrameHoverParams, metadata: CallMetadata): Promise<void> {
return await this._frame.hover(metadata, params.selector, params);
}
async selectOption(params: channels.FrameSelectOptionParams, metadata?: channels.Metadata): Promise<channels.FrameSelectOptionResult> {
return runAction(async controller => {
const elements = (params.elements || []).map(e => (e as ElementHandleDispatcher)._elementHandle);
return { values: await this._frame.selectOption(controller, params.selector, elements, params.options || [], params) };
}, { ...metadata, type: 'selectOption', target: params.selector, page: this._frame._page });
async selectOption(params: channels.FrameSelectOptionParams, metadata: CallMetadata): Promise<channels.FrameSelectOptionResult> {
const elements = (params.elements || []).map(e => (e as ElementHandleDispatcher)._elementHandle);
return { values: await this._frame.selectOption(metadata, params.selector, elements, params.options || [], params) };
}
async setInputFiles(params: channels.FrameSetInputFilesParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._frame.setInputFiles(controller, params.selector, params.files, params);
}, { ...metadata, type: 'setInputFiles', target: params.selector, page: this._frame._page });
async setInputFiles(params: channels.FrameSetInputFilesParams, metadata: CallMetadata): Promise<void> {
return await this._frame.setInputFiles(metadata, params.selector, params.files, params);
}
async type(params: channels.FrameTypeParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._frame.type(controller, params.selector, params.text, params);
}, { ...metadata, type: 'type', value: params.text, target: params.selector, page: this._frame._page });
async type(params: channels.FrameTypeParams, metadata: CallMetadata): Promise<void> {
return await this._frame.type(metadata, params.selector, params.text, params);
}
async press(params: channels.FramePressParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._frame.press(controller, params.selector, params.key, params);
}, { ...metadata, type: 'press', value: params.key, target: params.selector, page: this._frame._page });
async press(params: channels.FramePressParams, metadata: CallMetadata): Promise<void> {
return await this._frame.press(metadata, params.selector, params.key, params);
}
async check(params: channels.FrameCheckParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._frame.check(controller, params.selector, params);
}, { ...metadata, type: 'check', target: params.selector, page: this._frame._page });
async check(params: channels.FrameCheckParams, metadata: CallMetadata): Promise<void> {
return await this._frame.check(metadata, params.selector, params);
}
async uncheck(params: channels.FrameUncheckParams, metadata?: channels.Metadata): Promise<void> {
return runAction(async controller => {
return await this._frame.uncheck(controller, params.selector, params);
}, { ...metadata, type: 'uncheck', target: params.selector, page: this._frame._page });
async uncheck(params: channels.FrameUncheckParams, metadata: CallMetadata): Promise<void> {
return await this._frame.uncheck(metadata, params.selector, params);
}
async waitForFunction(params: channels.FrameWaitForFunctionParams): Promise<channels.FrameWaitForFunctionResult> {
return { handle: createHandle(this._scope, await this._frame._waitForFunctionExpression(params.expression, params.isFunction, parseArgument(params.arg), params)) };
async waitForFunction(params: channels.FrameWaitForFunctionParams, metadata: CallMetadata): Promise<channels.FrameWaitForFunctionResult> {
return { handle: createHandle(this._scope, await this._frame._waitForFunctionExpression(metadata, params.expression, params.isFunction, parseArgument(params.arg), params)) };
}
async title(): Promise<channels.FrameTitleResult> {
async title(params: channels.FrameTitleParams, metadata: CallMetadata): Promise<channels.FrameTitleResult> {
return { value: await this._frame.title() };
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { BrowserContext, runAction, Video } from '../server/browserContext';
import { BrowserContext, Video } from '../server/browserContext';
import { Frame } from '../server/frames';
import { Request } from '../server/network';
import { Page, Worker } from '../server/page';
@ -31,7 +31,7 @@ import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatche
import { FileChooser } from '../server/fileChooser';
import { CRCoverage } from '../server/chromium/crCoverage';
import { JSHandle } from '../server/javascript';
import { SdkObject } from '../server/sdkObject';
import { CallMetadata } from '../server/instrumentation';
export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> implements channels.PageChannel {
private _page: Page;
@ -80,19 +80,19 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) }));
}
async setDefaultNavigationTimeoutNoReply(params: channels.PageSetDefaultNavigationTimeoutNoReplyParams): Promise<void> {
async setDefaultNavigationTimeoutNoReply(params: channels.PageSetDefaultNavigationTimeoutNoReplyParams, metadata: CallMetadata): Promise<void> {
this._page.setDefaultNavigationTimeout(params.timeout);
}
async setDefaultTimeoutNoReply(params: channels.PageSetDefaultTimeoutNoReplyParams): Promise<void> {
async setDefaultTimeoutNoReply(params: channels.PageSetDefaultTimeoutNoReplyParams, metadata: CallMetadata): Promise<void> {
this._page.setDefaultTimeout(params.timeout);
}
async opener(): Promise<channels.PageOpenerResult> {
async opener(params: channels.PageOpenerParams, metadata: CallMetadata): Promise<channels.PageOpenerResult> {
return { page: lookupNullableDispatcher<PageDispatcher>(await this._page.opener()) };
}
async exposeBinding(params: channels.PageExposeBindingParams): Promise<void> {
async exposeBinding(params: channels.PageExposeBindingParams, metadata: CallMetadata): Promise<void> {
await this._page.exposeBinding(params.name, !!params.needsHandle, (source, ...args) => {
const binding = new BindingCallDispatcher(this._scope, params.name, !!params.needsHandle, source, args);
this._dispatchEvent('bindingCall', { binding });
@ -100,44 +100,38 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
});
}
async setExtraHTTPHeaders(params: channels.PageSetExtraHTTPHeadersParams): Promise<void> {
async setExtraHTTPHeaders(params: channels.PageSetExtraHTTPHeadersParams, metadata: CallMetadata): Promise<void> {
await this._page.setExtraHTTPHeaders(params.headers);
}
async reload(params: channels.PageReloadParams, metadata?: channels.Metadata): Promise<channels.PageReloadResult> {
return await runAction(async controller => {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.reload(controller, params)) };
}, { ...metadata, type: 'reload', page: this._page });
async reload(params: channels.PageReloadParams, metadata: CallMetadata): Promise<channels.PageReloadResult> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.reload(metadata, params)) };
}
async goBack(params: channels.PageGoBackParams, metadata?: channels.Metadata): Promise<channels.PageGoBackResult> {
return await runAction(async controller => {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goBack(controller, params)) };
}, { ...metadata, type: 'goBack', page: this._page });
async goBack(params: channels.PageGoBackParams, metadata: CallMetadata): Promise<channels.PageGoBackResult> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goBack(metadata, params)) };
}
async goForward(params: channels.PageGoForwardParams, metadata?: channels.Metadata): Promise<channels.PageGoForwardResult> {
return await runAction(async controller => {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goForward(controller, params)) };
}, { ...metadata, type: 'goForward', page: this._page });
async goForward(params: channels.PageGoForwardParams, metadata: CallMetadata): Promise<channels.PageGoForwardResult> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goForward(metadata, params)) };
}
async emulateMedia(params: channels.PageEmulateMediaParams): Promise<void> {
async emulateMedia(params: channels.PageEmulateMediaParams, metadata: CallMetadata): Promise<void> {
await this._page.emulateMedia({
media: params.media === 'null' ? null : params.media,
colorScheme: params.colorScheme === 'null' ? null : params.colorScheme,
});
}
async setViewportSize(params: channels.PageSetViewportSizeParams): Promise<void> {
async setViewportSize(params: channels.PageSetViewportSizeParams, metadata: CallMetadata): Promise<void> {
await this._page.setViewportSize(params.viewportSize);
}
async addInitScript(params: channels.PageAddInitScriptParams): Promise<void> {
async addInitScript(params: channels.PageAddInitScriptParams, metadata: CallMetadata): Promise<void> {
await this._page._addInitScriptExpression(params.source);
}
async setNetworkInterceptionEnabled(params: channels.PageSetNetworkInterceptionEnabledParams): Promise<void> {
async setNetworkInterceptionEnabled(params: channels.PageSetNetworkInterceptionEnabledParams, metadata: CallMetadata): Promise<void> {
if (!params.enabled) {
await this._page._setClientRequestInterceptor(undefined);
return;
@ -147,59 +141,59 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
});
}
async screenshot(params: channels.PageScreenshotParams): Promise<channels.PageScreenshotResult> {
return { binary: (await this._page.screenshot(params)).toString('base64') };
async screenshot(params: channels.PageScreenshotParams, metadata: CallMetadata): Promise<channels.PageScreenshotResult> {
return { binary: (await this._page.screenshot(metadata, params)).toString('base64') };
}
async close(params: channels.PageCloseParams): Promise<void> {
async close(params: channels.PageCloseParams, metadata: CallMetadata): Promise<void> {
await this._page.close(params);
}
async setFileChooserInterceptedNoReply(params: channels.PageSetFileChooserInterceptedNoReplyParams): Promise<void> {
async setFileChooserInterceptedNoReply(params: channels.PageSetFileChooserInterceptedNoReplyParams, metadata: CallMetadata): Promise<void> {
await this._page._setFileChooserIntercepted(params.intercepted);
}
async keyboardDown(params: channels.PageKeyboardDownParams): Promise<void> {
async keyboardDown(params: channels.PageKeyboardDownParams, metadata: CallMetadata): Promise<void> {
await this._page.keyboard.down(params.key);
}
async keyboardUp(params: channels.PageKeyboardUpParams): Promise<void> {
async keyboardUp(params: channels.PageKeyboardUpParams, metadata: CallMetadata): Promise<void> {
await this._page.keyboard.up(params.key);
}
async keyboardInsertText(params: channels.PageKeyboardInsertTextParams): Promise<void> {
async keyboardInsertText(params: channels.PageKeyboardInsertTextParams, metadata: CallMetadata): Promise<void> {
await this._page.keyboard.insertText(params.text);
}
async keyboardType(params: channels.PageKeyboardTypeParams): Promise<void> {
async keyboardType(params: channels.PageKeyboardTypeParams, metadata: CallMetadata): Promise<void> {
await this._page.keyboard.type(params.text, params);
}
async keyboardPress(params: channels.PageKeyboardPressParams): Promise<void> {
async keyboardPress(params: channels.PageKeyboardPressParams, metadata: CallMetadata): Promise<void> {
await this._page.keyboard.press(params.key, params);
}
async mouseMove(params: channels.PageMouseMoveParams): Promise<void> {
async mouseMove(params: channels.PageMouseMoveParams, metadata: CallMetadata): Promise<void> {
await this._page.mouse.move(params.x, params.y, params);
}
async mouseDown(params: channels.PageMouseDownParams): Promise<void> {
async mouseDown(params: channels.PageMouseDownParams, metadata: CallMetadata): Promise<void> {
await this._page.mouse.down(params);
}
async mouseUp(params: channels.PageMouseUpParams): Promise<void> {
async mouseUp(params: channels.PageMouseUpParams, metadata: CallMetadata): Promise<void> {
await this._page.mouse.up(params);
}
async mouseClick(params: channels.PageMouseClickParams): Promise<void> {
async mouseClick(params: channels.PageMouseClickParams, metadata: CallMetadata): Promise<void> {
await this._page.mouse.click(params.x, params.y, params);
}
async touchscreenTap(params: channels.PageTouchscreenTapParams): Promise<void> {
async touchscreenTap(params: channels.PageTouchscreenTapParams, metadata: CallMetadata): Promise<void> {
await this._page.touchscreen.tap(params.x, params.y);
}
async accessibilitySnapshot(params: channels.PageAccessibilitySnapshotParams): Promise<channels.PageAccessibilitySnapshotResult> {
async accessibilitySnapshot(params: channels.PageAccessibilitySnapshotParams, metadata: CallMetadata): Promise<channels.PageAccessibilitySnapshotResult> {
const rootAXNode = await this._page.accessibility.snapshot({
interestingOnly: params.interestingOnly,
root: params.root ? (params.root as ElementHandleDispatcher)._elementHandle : undefined
@ -207,33 +201,33 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
return { rootAXNode: rootAXNode || undefined };
}
async pdf(params: channels.PagePdfParams): Promise<channels.PagePdfResult> {
async pdf(params: channels.PagePdfParams, metadata: CallMetadata): Promise<channels.PagePdfResult> {
if (!this._page.pdf)
throw new Error('PDF generation is only supported for Headless Chromium');
const buffer = await this._page.pdf(params);
return { pdf: buffer.toString('base64') };
}
async bringToFront(): Promise<void> {
async bringToFront(params: channels.PageBringToFrontParams, metadata: CallMetadata): Promise<void> {
await this._page.bringToFront();
}
async crStartJSCoverage(params: channels.PageCrStartJSCoverageParams): Promise<void> {
async crStartJSCoverage(params: channels.PageCrStartJSCoverageParams, metadata: CallMetadata): Promise<void> {
const coverage = this._page.coverage as CRCoverage;
await coverage.startJSCoverage(params);
}
async crStopJSCoverage(): Promise<channels.PageCrStopJSCoverageResult> {
async crStopJSCoverage(params: channels.PageCrStopJSCoverageParams, metadata: CallMetadata): Promise<channels.PageCrStopJSCoverageResult> {
const coverage = this._page.coverage as CRCoverage;
return { entries: await coverage.stopJSCoverage() };
}
async crStartCSSCoverage(params: channels.PageCrStartCSSCoverageParams): Promise<void> {
async crStartCSSCoverage(params: channels.PageCrStartCSSCoverageParams, metadata: CallMetadata): Promise<void> {
const coverage = this._page.coverage as CRCoverage;
await coverage.startCSSCoverage(params);
}
async crStopCSSCoverage(): Promise<channels.PageCrStopCSSCoverageResult> {
async crStopCSSCoverage(params: channels.PageCrStopCSSCoverageParams, metadata: CallMetadata): Promise<channels.PageCrStopCSSCoverageResult> {
const coverage = this._page.coverage as CRCoverage;
return { entries: await coverage.stopCSSCoverage() };
}
@ -256,22 +250,22 @@ export class WorkerDispatcher extends Dispatcher<Worker, channels.WorkerInitiali
worker.on(Worker.Events.Close, () => this._dispatchEvent('close'));
}
async evaluateExpression(params: channels.WorkerEvaluateExpressionParams): Promise<channels.WorkerEvaluateExpressionResult> {
async evaluateExpression(params: channels.WorkerEvaluateExpressionParams, metadata: CallMetadata): Promise<channels.WorkerEvaluateExpressionResult> {
return { value: serializeResult(await this._object._evaluateExpression(params.expression, params.isFunction, parseArgument(params.arg))) };
}
async evaluateExpressionHandle(params: channels.WorkerEvaluateExpressionHandleParams): Promise<channels.WorkerEvaluateExpressionHandleResult> {
async evaluateExpressionHandle(params: channels.WorkerEvaluateExpressionHandleParams, metadata: CallMetadata): Promise<channels.WorkerEvaluateExpressionHandleResult> {
return { handle: createHandle(this._scope, await this._object._evaluateExpressionHandle(params.expression, params.isFunction, parseArgument(params.arg))) };
}
}
export class BindingCallDispatcher extends Dispatcher<SdkObject, channels.BindingCallInitializer> implements channels.BindingCallChannel {
export class BindingCallDispatcher extends Dispatcher<{}, channels.BindingCallInitializer> implements channels.BindingCallChannel {
private _resolve: ((arg: any) => void) | undefined;
private _reject: ((error: any) => void) | undefined;
private _promise: Promise<any>;
constructor(scope: DispatcherScope, name: string, needsHandle: boolean, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) {
super(scope, new SdkObject(null), 'BindingCall', {
super(scope, {}, 'BindingCall', {
frame: lookupDispatcher<FrameDispatcher>(source.frame),
name,
args: needsHandle ? undefined : args.map(serializeResult),
@ -287,11 +281,11 @@ export class BindingCallDispatcher extends Dispatcher<SdkObject, channels.Bindin
return this._promise;
}
async resolve(params: channels.BindingCallResolveParams): Promise<void> {
async resolve(params: channels.BindingCallResolveParams, metadata: CallMetadata): Promise<void> {
this._resolve!(parseArgument(params.result));
}
async reject(params: channels.BindingCallRejectParams): Promise<void> {
async reject(params: channels.BindingCallRejectParams, metadata: CallMetadata): Promise<void> {
this._reject!(parseError(params.error));
}
}

View File

@ -17,27 +17,18 @@
import * as channels from '../protocol/channels';
import { Dispatcher, DispatcherScope } from './dispatcher';
import * as stream from 'stream';
import { SdkObject } from '../server/sdkObject';
export class StreamWrapper extends SdkObject {
readonly stream: stream.Readable;
constructor(parentObject: SdkObject, stream: stream.Readable) {
super(parentObject);
this.stream = stream;
}
}
export class StreamDispatcher extends Dispatcher<StreamWrapper, channels.StreamInitializer> implements channels.StreamChannel {
constructor(scope: DispatcherScope, stream: StreamWrapper) {
export class StreamDispatcher extends Dispatcher<stream.Readable, channels.StreamInitializer> implements channels.StreamChannel {
constructor(scope: DispatcherScope, stream: stream.Readable) {
super(scope, stream, 'Stream', {});
}
async read(params: channels.StreamReadParams): Promise<channels.StreamReadResult> {
const buffer = this._object.stream.read(Math.min(this._object.stream.readableLength, params.size || this._object.stream.readableLength));
const buffer = this._object.read(Math.min(this._object.readableLength, params.size || this._object.readableLength));
return { binary: buffer ? buffer.toString('base64') : '' };
}
async close() {
this._object.stream.destroy();
this._object.destroy();
}
}

View File

@ -32,12 +32,12 @@ import { RecentLogsCollector } from '../../utils/debugLogger';
import { TimeoutSettings } from '../../utils/timeoutSettings';
import { AndroidWebView } from '../../protocol/channels';
import { CRPage } from '../chromium/crPage';
import { SdkObject } from '../sdkObject';
import { SdkObject, internalCallMetadata } from '../instrumentation';
const readFileAsync = util.promisify(fs.readFile);
export interface Backend {
devices(owner: SdkObject): Promise<DeviceBackend[]>;
devices(): Promise<DeviceBackend[]>;
}
export interface DeviceBackend {
@ -45,11 +45,11 @@ export interface DeviceBackend {
status: string;
close(): Promise<void>;
init(): Promise<void>;
runCommand(owner: SdkObject, command: string): Promise<Buffer>;
open(owner: SdkObject, command: string): Promise<SocketBackend>;
runCommand(command: string): Promise<Buffer>;
open(command: string): Promise<SocketBackend>;
}
export interface SocketBackend extends SdkObject {
export interface SocketBackend extends EventEmitter {
write(data: Buffer): Promise<void>;
close(): Promise<void>;
}
@ -72,7 +72,7 @@ export class Android extends SdkObject {
}
async devices(): Promise<AndroidDevice[]> {
const devices = (await this._backend.devices(this)).filter(d => d.status === 'device');
const devices = (await this._backend.devices()).filter(d => d.status === 'device');
const newSerials = new Set<string>();
for (const d of devices) {
newSerials.add(d.serial);
@ -125,7 +125,7 @@ export class AndroidDevice extends SdkObject {
static async create(android: Android, backend: DeviceBackend): Promise<AndroidDevice> {
await backend.init();
const model = await backend.runCommand(android, 'shell:getprop ro.product.model');
const model = await backend.runCommand('shell:getprop ro.product.model');
const device = new AndroidDevice(android, backend, model.toString().trim());
await device._init();
return device;
@ -144,17 +144,17 @@ export class AndroidDevice extends SdkObject {
}
async shell(command: string): Promise<Buffer> {
const result = await this._backend.runCommand(this, `shell:${command}`);
const result = await this._backend.runCommand(`shell:${command}`);
await this._refreshWebViews();
return result;
}
async open(command: string): Promise<SocketBackend> {
return await this._backend.open(this, `${command}`);
return await this._backend.open(`${command}`);
}
async screenshot(): Promise<Buffer> {
return await this._backend.runCommand(this, `shell:screencap -p`);
return await this._backend.runCommand(`shell:screencap -p`);
}
private async _driver(): Promise<Transport> {
@ -199,7 +199,7 @@ export class AndroidDevice extends SdkObject {
debug('pw:android')(`Polling the socket localabstract:${socketName}`);
while (!socket) {
try {
socket = await this._backend.open(this, `localabstract:${socketName}`);
socket = await this._backend.open(`localabstract:${socketName}`);
} catch (e) {
await new Promise(f => setTimeout(f, 250));
}
@ -235,13 +235,13 @@ export class AndroidDevice extends SdkObject {
async launchBrowser(pkg: string = 'com.android.chrome', options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
debug('pw:android')('Force-stopping', pkg);
await this._backend.runCommand(this, `shell:am force-stop ${pkg}`);
await this._backend.runCommand(`shell:am force-stop ${pkg}`);
const socketName = 'playwright-' + createGuid();
const commandLine = `_ --disable-fre --no-default-browser-check --no-first-run --remote-debugging-socket-name=${socketName}`;
debug('pw:android')('Starting', pkg, commandLine);
await this._backend.runCommand(this, `shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`);
await this._backend.runCommand(this, `shell:am start -n ${pkg}/com.google.android.apps.chrome.Main about:blank`);
await this._backend.runCommand(`shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`);
await this._backend.runCommand(`shell:am start -n ${pkg}/com.google.android.apps.chrome.Main about:blank`);
return await this._connectToBrowser(socketName, options);
}
@ -273,7 +273,7 @@ export class AndroidDevice extends SdkObject {
validateBrowserContextOptions(options, browserOptions);
const browser = await CRBrowser.connect(androidBrowser, browserOptions);
const controller = new ProgressController();
const controller = new ProgressController(internalCallMetadata(), this);
const defaultContext = browser._defaultContext!;
await controller.run(async progress => {
await defaultContext._loadDefaultContextAsIs(progress);
@ -296,7 +296,7 @@ export class AndroidDevice extends SdkObject {
async installApk(content: Buffer, options?: { args?: string[] }): Promise<void> {
const args = options && options.args ? options.args : ['-r', '-t', '-S'];
debug('pw:android')('Opening install socket');
const installSocket = await this._backend.open(this, `shell:cmd package install ${args.join(' ')} ${content.length}`);
const installSocket = await this._backend.open(`shell:cmd package install ${args.join(' ')} ${content.length}`);
debug('pw:android')('Writing driver bytes: ' + content.length);
await installSocket.write(content);
const success = await new Promise(f => installSocket.on('data', f));
@ -305,7 +305,7 @@ export class AndroidDevice extends SdkObject {
}
async push(content: Buffer, path: string, mode = 0o644): Promise<void> {
const socket = await this._backend.open(this, `sync:`);
const socket = await this._backend.open(`sync:`);
const sendHeader = async (command: string, length: number) => {
const buffer = Buffer.alloc(command.length + 4);
buffer.write(command, 0);
@ -329,7 +329,7 @@ export class AndroidDevice extends SdkObject {
}
private async _refreshWebViews() {
const sockets = (await this._backend.runCommand(this, `shell:cat /proc/net/unix | grep webview_devtools_remote`)).toString().split('\n');
const sockets = (await this._backend.runCommand(`shell:cat /proc/net/unix | grep webview_devtools_remote`)).toString().split('\n');
if (this._isClosed)
return;
@ -345,7 +345,7 @@ export class AndroidDevice extends SdkObject {
if (this._webViews.has(pid))
continue;
const procs = (await this._backend.runCommand(this, `shell:ps -A | grep ${pid}`)).toString().split('\n');
const procs = (await this._backend.runCommand(`shell:ps -A | grep ${pid}`)).toString().split('\n');
if (this._isClosed)
return;
let pkg = '';

View File

@ -17,12 +17,12 @@
import * as assert from 'assert';
import * as debug from 'debug';
import * as net from 'net';
import { SdkObject } from '../sdkObject';
import { EventEmitter } from 'events';
import { Backend, DeviceBackend, SocketBackend } from './android';
export class AdbBackend implements Backend {
async devices(sdkObject: SdkObject): Promise<DeviceBackend[]> {
const result = await runCommand(sdkObject, 'host:devices');
async devices(): Promise<DeviceBackend[]> {
const result = await runCommand('host:devices');
const lines = result.toString().trim().split('\n');
return lines.map(line => {
const [serial, status] = line.trim().split('\t');
@ -46,20 +46,20 @@ class AdbDevice implements DeviceBackend {
async close() {
}
runCommand(sdkObject: SdkObject, command: string): Promise<Buffer> {
return runCommand(sdkObject, command, this.serial);
runCommand(command: string): Promise<Buffer> {
return runCommand(command, this.serial);
}
async open(sdkObject: SdkObject, command: string): Promise<SocketBackend> {
const result = await open(sdkObject, command, this.serial);
async open(command: string): Promise<SocketBackend> {
const result = await open(command, this.serial);
result.becomeSocket();
return result;
}
}
async function runCommand(sdkObject: SdkObject, command: string, serial?: string): Promise<Buffer> {
async function runCommand(command: string, serial?: string): Promise<Buffer> {
debug('pw:adb:runCommand')(command, serial);
const socket = new BufferedSocketWrapper(sdkObject, command, net.createConnection({ port: 5037 }));
const socket = new BufferedSocketWrapper(command, net.createConnection({ port: 5037 }));
if (serial) {
await socket.write(encodeMessage(`host:transport:${serial}`));
const status = await socket.read(4);
@ -79,8 +79,8 @@ async function runCommand(sdkObject: SdkObject, command: string, serial?: string
return commandOutput;
}
async function open(sdkObject: SdkObject, command: string, serial?: string): Promise<BufferedSocketWrapper> {
const socket = new BufferedSocketWrapper(sdkObject, command, net.createConnection({ port: 5037 }));
async function open(command: string, serial?: string): Promise<BufferedSocketWrapper> {
const socket = new BufferedSocketWrapper(command, net.createConnection({ port: 5037 }));
if (serial) {
await socket.write(encodeMessage(`host:transport:${serial}`));
const status = await socket.read(4);
@ -98,7 +98,7 @@ function encodeMessage(message: string): Buffer {
return Buffer.from(lenHex + message);
}
class BufferedSocketWrapper extends SdkObject implements SocketBackend {
class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
private _socket: net.Socket;
private _buffer = Buffer.from([]);
private _isSocket = false;
@ -107,8 +107,8 @@ class BufferedSocketWrapper extends SdkObject implements SocketBackend {
private _isClosed = false;
private _command: string;
constructor(parent: SdkObject, command: string, socket: net.Socket) {
super(parent);
constructor(command: string, socket: net.Socket) {
super();
this._command = command;
this._socket = socket;
this._connectPromise = new Promise(f => this._socket.on('connect', f));

View File

@ -15,15 +15,14 @@
*/
import * as types from './types';
import { BrowserContext, ContextListener, Video } from './browserContext';
import { BrowserContext, Video } from './browserContext';
import { Page } from './page';
import { Download } from './download';
import { ProxySettings } from './types';
import { ChildProcess } from 'child_process';
import { RecentLogsCollector } from '../utils/debugLogger';
import * as registry from '../utils/registry';
import { SdkObject } from './sdkObject';
import { Selectors } from './selectors';
import { SdkObject } from './instrumentation';
export interface BrowserProcess {
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
@ -33,12 +32,9 @@ export interface BrowserProcess {
}
export type PlaywrightOptions = {
contextListeners: ContextListener[],
registry: registry.Registry,
isInternal: boolean,
rootSdkObject: SdkObject,
// FIXME, this is suspicious
selectors: Selectors
};
export type BrowserOptions = PlaywrightOptions & {

View File

@ -18,17 +18,16 @@
import { TimeoutSettings } from '../utils/timeoutSettings';
import { mkdirIfNeeded } from '../utils/utils';
import { Browser, BrowserOptions } from './browser';
import * as dom from './dom';
import { Download } from './download';
import * as frames from './frames';
import { helper } from './helper';
import * as network from './network';
import { Page, PageBinding, PageDelegate } from './page';
import { Progress, ProgressController, ProgressResult } from './progress';
import { Selectors } from './selectors';
import { Progress } from './progress';
import { Selectors, serverSelectors } from './selectors';
import * as types from './types';
import * as path from 'path';
import { SdkObject } from './sdkObject';
import { CallMetadata, SdkObject } from './instrumentation';
export class Video {
readonly _videoId: string;
@ -58,42 +57,6 @@ export class Video {
}
}
export type ActionMetadata = {
type: 'click' | 'fill' | 'dblclick' | 'hover' | 'selectOption' | 'setInputFiles' | 'type' | 'press' | 'check' | 'uncheck' | 'goto' | 'setContent' | 'goBack' | 'goForward' | 'reload' | 'tap',
page: Page,
target?: dom.ElementHandle | string,
value?: string,
stack?: string,
};
export interface ActionListener {
onActionCheckpoint(name: string, metadata: ActionMetadata): Promise<void>;
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({
onProgressCheckpoint: async (name: string): Promise<void> => {
for (const listener of metadata.page._browserContext._actionListeners)
await listener.onActionCheckpoint(name, metadata);
},
onProgressDone: async (result: ProgressResult): Promise<void> => {
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>;
onContextWillDestroy(context: BrowserContext): Promise<void>;
onContextDidDestroy(context: BrowserContext): Promise<void>;
}
export abstract class BrowserContext extends SdkObject {
static Events = {
Close: 'close',
@ -117,7 +80,6 @@ export abstract class BrowserContext extends SdkObject {
readonly _browser: Browser;
readonly _browserContextId: string | undefined;
private _selectors?: Selectors;
readonly _actionListeners = new Set<ActionListener>();
private _origins = new Set<string>();
terminalSize: { rows?: number, columns?: number } = {};
@ -136,12 +98,11 @@ export abstract class BrowserContext extends SdkObject {
}
selectors(): Selectors {
return this._selectors || this._browser.options.selectors;
return this._selectors || serverSelectors;
}
async _initialize() {
for (const listener of this._browser.options.contextListeners)
await listener.onContextCreated(this);
await this.instrumentation.onContextCreated(this);
}
async _ensureVideosPath() {
@ -292,8 +253,7 @@ export abstract class BrowserContext extends SdkObject {
this.emit(BrowserContext.Events.BeforeClose);
this._closedStatus = 'closing';
for (const listener of this._browser.options.contextListeners)
await listener.onContextWillDestroy(this);
await this.instrumentation.onContextWillDestroy(this);
// Collect videos/downloads that we will await.
const promises: Promise<any>[] = [];
@ -321,8 +281,7 @@ export abstract class BrowserContext extends SdkObject {
await this._browser.close();
// Bookkeeping.
for (const listener of this._browser.options.contextListeners)
await listener.onContextDidDestroy(this);
await this.instrumentation.onContextWillDestroy(this);
this._didCloseInternal();
}
await this._closePromise;
@ -343,7 +302,7 @@ export abstract class BrowserContext extends SdkObject {
this._origins.add(origin);
}
async storageState(): Promise<types.StorageState> {
async storageState(metadata: CallMetadata): Promise<types.StorageState> {
const result: types.StorageState = {
cookies: (await this.cookies()).filter(c => c.value !== ''),
origins: []
@ -357,7 +316,7 @@ export abstract class BrowserContext extends SdkObject {
const originStorage: types.OriginStorage = { origin, localStorage: [] };
result.origins.push(originStorage);
const frame = page.mainFrame();
await frame.goto(new ProgressController(), origin);
await frame.goto(metadata, origin);
const storage = await frame._evaluateExpression(`({
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
})`, false, undefined, 'utility');
@ -368,7 +327,7 @@ export abstract class BrowserContext extends SdkObject {
return result;
}
async setStorageState(state: types.SetStorageState) {
async setStorageState(metadata: CallMetadata, state: types.SetStorageState) {
if (state.cookies)
await this.addCookies(state.cookies);
if (state.origins && state.origins.length) {
@ -378,7 +337,7 @@ export abstract class BrowserContext extends SdkObject {
});
for (const originState of state.origins) {
const frame = page.mainFrame();
await frame.goto(new ProgressController(), originState.origin);
await frame.goto(metadata, originState.origin);
await frame._evaluateExpression(`
originState => {
for (const { name, value } of (originState.localStorage || []))

View File

@ -31,7 +31,7 @@ import { validateHostRequirements } from './validateDependencies';
import { isDebugMode } from '../utils/utils';
import { helper } from './helper';
import { RecentLogsCollector } from '../utils/debugLogger';
import { SdkObject } from './sdkObject';
import { CallMetadata, SdkObject } from './instrumentation';
const mkdirAsync = util.promisify(fs.mkdir);
const mkdtempAsync = util.promisify(fs.mkdtemp);
@ -59,9 +59,9 @@ export abstract class BrowserType extends SdkObject {
return this._name;
}
async launch(options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise<Browser> {
async launch(metadata: CallMetadata, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise<Browser> {
options = validateLaunchOptions(options);
const controller = new ProgressController();
const controller = new ProgressController(metadata, this);
controller.setLogName('browser');
const browser = await controller.run(progress => {
return this._innerLaunchWithRetries(progress, options, undefined, helper.debugProtocolLogger(protocolLogger)).catch(e => { throw this._rewriteStartupError(e); });
@ -69,10 +69,10 @@ export abstract class BrowserType extends SdkObject {
return browser;
}
async launchPersistentContext(userDataDir?: string, options: types.LaunchPersistentOptions = {}): Promise<BrowserContext> {
async launchPersistentContext(metadata: CallMetadata, userDataDir?: string, options: types.LaunchPersistentOptions = {}): Promise<BrowserContext> {
options = validateLaunchOptions(options);
const controller = new ProgressController(metadata, this);
const persistent: types.BrowserContextOptions = options;
const controller = new ProgressController();
controller.setLogName('browser');
const browser = await controller.run(progress => {
return this._innerLaunchWithRetries(progress, options, persistent, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupError(e); });

View File

@ -23,7 +23,6 @@ import { rewriteErrorMessage } from '../../utils/stackTrace';
import { debugLogger, RecentLogsCollector } from '../../utils/debugLogger';
import { ProtocolLogger } from '../types';
import { helper } from '../helper';
import { SdkObject } from '../sdkObject';
export const ConnectionEvents = {
Disconnected: Symbol('ConnectionEvents.Disconnected')
@ -124,7 +123,7 @@ export const CRSessionEvents = {
Disconnected: Symbol('Events.CDPSession.Disconnected')
};
export class CRSession extends SdkObject {
export class CRSession extends EventEmitter {
_connection: CRConnection | null;
_eventListener?: (method: string, params?: Object) => void;
private readonly _callbacks = new Map<number, {resolve: (o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
@ -140,7 +139,8 @@ export class CRSession extends SdkObject {
once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
constructor(connection: CRConnection, rootSessionId: string, targetType: string, sessionId: string) {
super(null);
super();
this.setMaxListeners(0);
this._connection = connection;
this._rootSessionId = rootSessionId;
this._targetType = targetType;

View File

@ -759,7 +759,7 @@ class FrameSession {
lineNumber: lineNumber || 0,
columnNumber: 0,
};
this._page.emit(Page.Events.Console, new ConsoleMessage(this._page, level, text, [], location));
this._page.emit(Page.Events.Console, new ConsoleMessage(level, text, [], location));
}
}
@ -814,7 +814,7 @@ class FrameSession {
const ffmpegPath = this._crPage._browserContext._browser.options.registry.executablePath('ffmpeg');
if (!ffmpegPath)
throw new Error('ffmpeg executable was not found');
this._videoRecorder = await VideoRecorder.launch(ffmpegPath, options);
this._videoRecorder = await VideoRecorder.launch(this._crPage._page, ffmpegPath, options);
this._screencastId = screencastId;
const gotFirstFrame = new Promise(f => this._client.once('Page.screencastFrame', f));
await this._client.send('Page.startScreencast', {

View File

@ -16,8 +16,10 @@
import { ChildProcess } from 'child_process';
import { assert, monotonicTime } from '../../utils/utils';
import { Page } from '../page';
import { launchProcess } from '../processLauncher';
import { Progress, ProgressController } from '../progress';
import { internalCallMetadata } from '../instrumentation';
import * as types from '../types';
const fps = 25;
@ -34,11 +36,11 @@ export class VideoRecorder {
private _isStopped = false;
private _ffmpegPath: string;
static async launch(ffmpegPath: string, options: types.PageScreencastOptions): Promise<VideoRecorder> {
static async launch(page: Page, ffmpegPath: string, options: types.PageScreencastOptions): Promise<VideoRecorder> {
if (!options.outputFile.endsWith('.webm'))
throw new Error('File must have .webm extension');
const controller = new ProgressController();
const controller = new ProgressController(internalCallMetadata(), page);
controller.setLogName('browser');
return await controller.run(async progress => {
const recorder = new VideoRecorder(ffmpegPath, progress);

View File

@ -15,17 +15,15 @@
*/
import * as js from './javascript';
import { SdkObject } from './sdkObject';
import { ConsoleMessageLocation } from './types';
export class ConsoleMessage extends SdkObject {
export class ConsoleMessage {
private _type: string;
private _text?: string;
private _args: js.JSHandle[];
private _location: ConsoleMessageLocation;
constructor(parent: SdkObject, type: string, text: string | undefined, args: js.JSHandle[], location?: ConsoleMessageLocation) {
super(parent);
constructor(type: string, text: string | undefined, args: js.JSHandle[], location?: ConsoleMessageLocation) {
this._type = type;
this._text = text;
this._args = args;

View File

@ -17,7 +17,7 @@
import { assert } from '../utils/utils';
import { Page } from './page';
import { SdkObject } from './sdkObject';
import { SdkObject } from './instrumentation';
type OnHandle = (accept: boolean, promptText?: string) => Promise<void>;

View File

@ -22,8 +22,9 @@ import * as js from './javascript';
import { Page } from './page';
import { SelectorInfo } from './selectors';
import * as types from './types';
import { Progress, ProgressController, runAbortableTask } from './progress';
import { Progress, ProgressController } from './progress';
import { FatalDOMError, RetargetableDOMError } from './common/domErrors';
import { CallMetadata } from './instrumentation';
export class FrameExecutionContext extends js.ExecutionContext {
readonly frame: frames.Frame;
@ -214,8 +215,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
}
async scrollIntoViewIfNeeded(options: types.TimeoutOptions = {}) {
return runAbortableTask(
async scrollIntoViewIfNeeded(metadata: CallMetadata, options: types.TimeoutOptions = {}) {
const controller = new ProgressController(metadata, this);
return controller.run(
progress => this._waitAndScrollIntoViewIfNeeded(progress),
this._page._timeoutSettings.timeout(options));
}
@ -392,7 +394,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return 'done';
}
async hover(controller: ProgressController, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise<void> {
async hover(metadata: CallMetadata, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise<void> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
const result = await this._hover(progress, options);
return assertDone(throwRetargetableDOMError(result));
@ -403,7 +406,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._retryPointerAction(progress, 'hover', false /* waitForEnabled */, point => this._page.mouse.move(point.x, point.y), options);
}
async click(controller: ProgressController, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
async click(metadata: CallMetadata, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
const result = await this._click(progress, options);
return assertDone(throwRetargetableDOMError(result));
@ -414,7 +418,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._retryPointerAction(progress, 'click', true /* waitForEnabled */, point => this._page.mouse.click(point.x, point.y, options), options);
}
async dblclick(controller: ProgressController, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
async dblclick(metadata: CallMetadata, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
const result = await this._dblclick(progress, options);
return assertDone(throwRetargetableDOMError(result));
@ -425,7 +430,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._retryPointerAction(progress, 'dblclick', true /* waitForEnabled */, point => this._page.mouse.dblclick(point.x, point.y, options), options);
}
async tap(controller: ProgressController, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
async tap(metadata: CallMetadata, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
const result = await this._tap(progress, options);
return assertDone(throwRetargetableDOMError(result));
@ -436,7 +442,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._retryPointerAction(progress, 'tap', true /* waitForEnabled */, point => this._page.touchscreen.tap(point.x, point.y), options);
}
async selectOption(controller: ProgressController, elements: ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions): Promise<string[]> {
async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions): Promise<string[]> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
const result = await this._selectOption(progress, elements, values, options);
return throwRetargetableDOMError(result);
@ -457,7 +464,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
});
}
async fill(controller: ProgressController, value: string, options: types.NavigatingActionWaitOptions = {}): Promise<void> {
async fill(metadata: CallMetadata, value: string, options: types.NavigatingActionWaitOptions = {}): Promise<void> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
const result = await this._fill(progress, value, options);
assertDone(throwRetargetableDOMError(result));
@ -491,8 +499,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, 'input');
}
async selectText(options: types.TimeoutOptions = {}): Promise<void> {
return runAbortableTask(async progress => {
async selectText(metadata: CallMetadata, options: types.TimeoutOptions = {}): Promise<void> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
progress.throwIfAborted(); // Avoid action that has side-effects.
const poll = await this._evaluateHandleInUtility(([injected, node]) => {
return injected.waitForVisibleAndSelectText(node);
@ -503,7 +512,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, this._page._timeoutSettings.timeout(options));
}
async setInputFiles(controller: ProgressController, files: types.FilePayload[], options: types.NavigatingActionWaitOptions) {
async setInputFiles(metadata: CallMetadata, files: types.FilePayload[], options: types.NavigatingActionWaitOptions) {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
const result = await this._setInputFiles(progress, files, options);
return assertDone(throwRetargetableDOMError(result));
@ -531,8 +541,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return 'done';
}
async focus(): Promise<void> {
await runAbortableTask(async progress => {
async focus(metadata: CallMetadata): Promise<void> {
const controller = new ProgressController(metadata, this);
await controller.run(async progress => {
const result = await this._focus(progress);
await this._page._doSlowMo();
return assertDone(throwRetargetableDOMError(result));
@ -545,7 +556,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return throwFatalDOMError(result);
}
async type(controller: ProgressController, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<void> {
async type(metadata: CallMetadata, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<void> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
const result = await this._type(progress, text, options);
return assertDone(throwRetargetableDOMError(result));
@ -565,7 +577,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, 'input');
}
async press(controller: ProgressController, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<void> {
async press(metadata: CallMetadata, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<void> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
const result = await this._press(progress, key, options);
return assertDone(throwRetargetableDOMError(result));
@ -585,14 +598,16 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, 'input');
}
async check(controller: ProgressController, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
async check(metadata: CallMetadata, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
const result = await this._setChecked(progress, true, options);
return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options));
}
async uncheck(controller: ProgressController, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
async uncheck(metadata: CallMetadata, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
const result = await this._setChecked(progress, false, options);
return assertDone(throwRetargetableDOMError(result));
@ -614,8 +629,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._page._delegate.getBoundingBox(this);
}
async screenshot(options: types.ElementScreenshotOptions = {}): Promise<Buffer> {
return runAbortableTask(
async screenshot(metadata: CallMetadata, options: types.ElementScreenshotOptions = {}): Promise<Buffer> {
const controller = new ProgressController(metadata, this);
return controller.run(
progress => this._page._screenshotter.screenshotElement(progress, this, options),
this._page._timeoutSettings.timeout(options));
}
@ -679,8 +695,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, {});
}
async waitForElementState(state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled' | 'editable', options: types.TimeoutOptions = {}): Promise<void> {
return runAbortableTask(async progress => {
async waitForElementState(metadata: CallMetadata, state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled' | 'editable', options: types.TimeoutOptions = {}): Promise<void> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
progress.log(` waiting for element to be ${state}`);
if (state === 'visible') {
const poll = await this._evaluateHandleInUtility(([injected, node]) => {
@ -735,13 +752,14 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, this._page._timeoutSettings.timeout(options));
}
async waitForSelector(selector: string, options: types.WaitForElementOptions = {}): Promise<ElementHandle<Element> | null> {
async waitForSelector(metadata: CallMetadata, selector: string, options: types.WaitForElementOptions = {}): Promise<ElementHandle<Element> | null> {
const { state = 'visible' } = options;
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
const info = this._page.selectors._parseSelector(selector);
const task = waitForSelectorTask(info, state, this);
return runAbortableTask(async progress => {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
const context = await this._context.frame._context(info.world);
const injected = await context.injectedScript();

View File

@ -19,11 +19,10 @@ import * as fs from 'fs';
import * as util from 'util';
import { Page } from './page';
import { assert } from '../utils/utils';
import { SdkObject } from './sdkObject';
type SaveCallback = (localPath: string, error?: string) => Promise<void>;
export class Download extends SdkObject {
export class Download {
private _downloadsPath: string;
private _uuid: string;
private _finishedCallback: () => void;
@ -38,7 +37,6 @@ export class Download extends SdkObject {
private _suggestedFilename: string | undefined;
constructor(page: Page, downloadsPath: string, uuid: string, url: string, suggestedFilename?: string) {
super(page);
this._page = page;
this._downloadsPath = downloadsPath;
this._uuid = uuid;

View File

@ -26,13 +26,13 @@ import * as types from '../types';
import { launchProcess, envArrayToObject } from '../processLauncher';
import { BrowserContext } from '../browserContext';
import type {BrowserWindow} from 'electron';
import { Progress, ProgressController, runAbortableTask } from '../progress';
import { Progress, ProgressController } from '../progress';
import { helper } from '../helper';
import { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser';
import * as childProcess from 'child_process';
import * as readline from 'readline';
import { RecentLogsCollector } from '../../utils/debugLogger';
import { SdkObject } from '../sdkObject';
import { internalCallMetadata, SdkObject } from '../instrumentation';
export type ElectronLaunchOptionsBase = {
executablePath?: string,
@ -89,7 +89,8 @@ export class ElectronApplication extends SdkObject {
if (!handle)
return;
page.browserWindow = handle;
await runAbortableTask(progress => page.mainFrame()._waitForLoadState(progress, 'domcontentloaded'), page._timeoutSettings.navigationTimeout({})).catch(e => {}); // can happen after detach
const controller = new ProgressController(internalCallMetadata(), this);
await controller.run(progress => page.mainFrame()._waitForLoadState(progress, 'domcontentloaded'), page._timeoutSettings.navigationTimeout({})).catch(e => {}); // can happen after detach
this.emit(ElectronApplication.Events.Window, page);
}
@ -105,7 +106,7 @@ export class ElectronApplication extends SdkObject {
}
private async _waitForEvent(event: string, predicate?: Function): Promise<any> {
const progressController = new ProgressController();
const progressController = new ProgressController(internalCallMetadata(), this);
if (event !== ElectronApplication.Events.Close)
this._browserContext._closePromise.then(error => progressController.abort(error));
return progressController.run(progress => helper.waitForEvent(progress, this, event, predicate).promise, this._timeoutSettings.timeout({}));
@ -133,7 +134,7 @@ export class Electron extends SdkObject {
const {
args = [],
} = options;
const controller = new ProgressController();
const controller = new ProgressController(internalCallMetadata(), this);
controller.setLogName('browser');
return controller.run(async progress => {
let app: ElectronApplication | undefined = undefined;

View File

@ -23,10 +23,10 @@ import * as network from './network';
import { Page } from './page';
import * as types from './types';
import { BrowserContext } from './browserContext';
import { Progress, ProgressController, runAbortableTask } from './progress';
import { Progress, ProgressController } from './progress';
import { assert, makeWaitForNextTask } from '../utils/utils';
import { debugLogger } from '../utils/debugLogger';
import { SdkObject } from './sdkObject';
import { CallMetadata, SdkObject } from './instrumentation';
type ContextData = {
contextPromise: Promise<dom.FrameExecutionContext>;
@ -480,14 +480,16 @@ export class Frame extends SdkObject {
this._subtreeLifecycleEvents = events;
}
setupNavigationProgressController(controller: ProgressController) {
setupNavigationProgressController(metadata: CallMetadata): ProgressController {
const controller = new ProgressController(metadata, this);
this._page._disconnectedPromise.then(() => controller.abort(new Error('Navigation failed because page was closed!')));
this._page._crashedPromise.then(() => controller.abort(new Error('Navigation failed because page crashed!')));
this._detachedPromise.then(() => controller.abort(new Error('Navigating frame was detached!')));
return controller;
}
async goto(controller: ProgressController, url: string, options: types.GotoOptions = {}): Promise<network.Response | null> {
this.setupNavigationProgressController(controller);
async goto(metadata: CallMetadata, url: string, options: types.GotoOptions = {}): Promise<network.Response | null> {
const controller = this.setupNavigationProgressController(metadata);
return controller.run(async progress => {
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
progress.log(`navigating to "${url}", waiting until "${waitUntil}"`);
@ -599,7 +601,8 @@ export class Frame extends SdkObject {
return this._page.selectors._query(this, selector);
}
async waitForSelector(selector: string, options: types.WaitForElementOptions = {}): Promise<dom.ElementHandle<Element> | null> {
async waitForSelector(metadata: CallMetadata, selector: string, options: types.WaitForElementOptions = {}): Promise<dom.ElementHandle<Element> | null> {
const controller = new ProgressController(metadata, this);
if ((options as any).visibility)
throw new Error('options.visibility is not supported, did you mean options.state?');
if ((options as any).waitFor && (options as any).waitFor !== 'visible')
@ -609,7 +612,7 @@ export class Frame extends SdkObject {
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
const info = this._page.selectors._parseSelector(selector);
const task = dom.waitForSelectorTask(info, state);
return runAbortableTask(async progress => {
return controller.run(async progress => {
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
const result = await this._scheduleRerunnableHandleTask(progress, info.world, task);
if (!result.asElement()) {
@ -621,10 +624,11 @@ export class Frame extends SdkObject {
}, this._page._timeoutSettings.timeout(options));
}
async dispatchEvent(selector: string, type: string, eventInit?: Object, options: types.TimeoutOptions = {}): Promise<void> {
async dispatchEvent(metadata: CallMetadata, selector: string, type: string, eventInit?: Object, options: types.TimeoutOptions = {}): Promise<void> {
const controller = new ProgressController(metadata, this);
const info = this._page.selectors._parseSelector(selector);
const task = dom.dispatchEventTask(info, type, eventInit || {});
await runAbortableTask(async progress => {
await controller.run(async progress => {
progress.log(`Dispatching "${type}" event on selector "${selector}"...`);
// Note: we always dispatch events in the main world.
await this._scheduleRerunnableTask(progress, 'main', task);
@ -664,8 +668,8 @@ export class Frame extends SdkObject {
});
}
async setContent(controller: ProgressController, html: string, options: types.NavigateOptions = {}): Promise<void> {
this.setupNavigationProgressController(controller);
async setContent(metadata: CallMetadata, html: string, options: types.NavigateOptions = {}): Promise<void> {
const controller = this.setupNavigationProgressController(metadata);
return controller.run(async progress => {
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
progress.log(`setting frame content, waiting until "${waitUntil}"`);
@ -857,166 +861,188 @@ export class Frame extends SdkObject {
}
private async _retryWithSelectorIfNotConnected<R>(
controller: ProgressController,
selector: string, options: types.TimeoutOptions,
action: (progress: Progress, handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> {
return runAbortableTask(async progress => {
return controller.run(async progress => {
return this._retryWithProgressIfNotConnected(progress, selector, handle => action(progress, handle));
}, this._page._timeoutSettings.timeout(options));
}
async click(controller: ProgressController, selector: string, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
async click(metadata: CallMetadata, selector: string, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._click(progress, options)));
}, this._page._timeoutSettings.timeout(options));
}
async dblclick(controller: ProgressController, selector: string, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
async dblclick(metadata: CallMetadata, selector: string, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._dblclick(progress, options)));
}, this._page._timeoutSettings.timeout(options));
}
async tap(controller: ProgressController, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
async tap(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._tap(progress, options)));
}, this._page._timeoutSettings.timeout(options));
}
async fill(controller: ProgressController, selector: string, value: string, options: types.NavigatingActionWaitOptions) {
async fill(metadata: CallMetadata, selector: string, value: string, options: types.NavigatingActionWaitOptions) {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._fill(progress, value, options)));
}, this._page._timeoutSettings.timeout(options));
}
async focus(selector: string, options: types.TimeoutOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._focus(progress));
async focus(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}) {
const controller = new ProgressController(metadata, this);
await this._retryWithSelectorIfNotConnected(controller, selector, options, (progress, handle) => handle._focus(progress));
await this._page._doSlowMo();
}
async textContent(selector: string, options: types.TimeoutOptions = {}): Promise<string | null> {
async textContent(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<string | null> {
const controller = new ProgressController(metadata, this);
const info = this._page.selectors._parseSelector(selector);
const task = dom.textContentTask(info);
return runAbortableTask(async progress => {
return controller.run(async progress => {
progress.log(` retrieving textContent from "${selector}"`);
return this._scheduleRerunnableTask(progress, info.world, task);
}, this._page._timeoutSettings.timeout(options));
}
async innerText(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
async innerText(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<string> {
const controller = new ProgressController(metadata, this);
const info = this._page.selectors._parseSelector(selector);
const task = dom.innerTextTask(info);
return runAbortableTask(async progress => {
return controller.run(async progress => {
progress.log(` retrieving innerText from "${selector}"`);
const result = dom.throwFatalDOMError(await this._scheduleRerunnableTask(progress, info.world, task));
return result.innerText;
}, this._page._timeoutSettings.timeout(options));
}
async innerHTML(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
async innerHTML(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<string> {
const controller = new ProgressController(metadata, this);
const info = this._page.selectors._parseSelector(selector);
const task = dom.innerHTMLTask(info);
return runAbortableTask(async progress => {
return controller.run(async progress => {
progress.log(` retrieving innerHTML from "${selector}"`);
return this._scheduleRerunnableTask(progress, info.world, task);
}, this._page._timeoutSettings.timeout(options));
}
async getAttribute(selector: string, name: string, options: types.TimeoutOptions = {}): Promise<string | null> {
async getAttribute(metadata: CallMetadata, selector: string, name: string, options: types.TimeoutOptions = {}): Promise<string | null> {
const controller = new ProgressController(metadata, this);
const info = this._page.selectors._parseSelector(selector);
const task = dom.getAttributeTask(info, name);
return runAbortableTask(async progress => {
return controller.run(async progress => {
progress.log(` retrieving attribute "${name}" from "${selector}"`);
return this._scheduleRerunnableTask(progress, info.world, task);
}, this._page._timeoutSettings.timeout(options));
}
async isVisible(selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
async isVisible(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
const controller = new ProgressController(metadata, this);
const info = this._page.selectors._parseSelector(selector);
const task = dom.visibleTask(info);
return runAbortableTask(async progress => {
return controller.run(async progress => {
progress.log(` checking visibility of "${selector}"`);
return this._scheduleRerunnableTask(progress, info.world, task);
}, this._page._timeoutSettings.timeout(options));
}
async isHidden(selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
return !(await this.isVisible(selector, options));
async isHidden(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
return !(await this.isVisible(metadata, selector, options));
}
async isDisabled(selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
async isDisabled(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
const controller = new ProgressController(metadata, this);
const info = this._page.selectors._parseSelector(selector);
const task = dom.disabledTask(info);
return runAbortableTask(async progress => {
return controller.run(async progress => {
progress.log(` checking disabled state of "${selector}"`);
return this._scheduleRerunnableTask(progress, info.world, task);
}, this._page._timeoutSettings.timeout(options));
}
async isEnabled(selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
return !(await this.isDisabled(selector, options));
async isEnabled(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
return !(await this.isDisabled(metadata, selector, options));
}
async isEditable(selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
async isEditable(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
const controller = new ProgressController(metadata, this);
const info = this._page.selectors._parseSelector(selector);
const task = dom.editableTask(info);
return runAbortableTask(async progress => {
return controller.run(async progress => {
progress.log(` checking editable state of "${selector}"`);
return this._scheduleRerunnableTask(progress, info.world, task);
}, this._page._timeoutSettings.timeout(options));
}
async isChecked(selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
async isChecked(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
const controller = new ProgressController(metadata, this);
const info = this._page.selectors._parseSelector(selector);
const task = dom.checkedTask(info);
return runAbortableTask(async progress => {
return controller.run(async progress => {
progress.log(` checking checked state of "${selector}"`);
return this._scheduleRerunnableTask(progress, info.world, task);
}, this._page._timeoutSettings.timeout(options));
}
async hover(controller: ProgressController, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) {
async hover(metadata: CallMetadata, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._hover(progress, options)));
}, this._page._timeoutSettings.timeout(options));
}
async selectOption(controller: ProgressController, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
return await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._selectOption(progress, elements, values, options));
}, this._page._timeoutSettings.timeout(options));
}
async setInputFiles(controller: ProgressController, selector: string, files: types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise<void> {
async setInputFiles(metadata: CallMetadata, selector: string, files: types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise<void> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._setInputFiles(progress, files, options)));
}, this._page._timeoutSettings.timeout(options));
}
async type(controller: ProgressController, selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
async type(metadata: CallMetadata, selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._type(progress, text, options)));
}, this._page._timeoutSettings.timeout(options));
}
async press(controller: ProgressController, selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
async press(metadata: CallMetadata, selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._press(progress, key, options)));
}, this._page._timeoutSettings.timeout(options));
}
async check(controller: ProgressController, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
async check(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._setChecked(progress, true, options)));
}, this._page._timeoutSettings.timeout(options));
}
async uncheck(controller: ProgressController, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
async uncheck(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._setChecked(progress, false, options)));
}, this._page._timeoutSettings.timeout(options));
}
async _waitForFunctionExpression<R>(expression: string, isFunction: boolean | undefined, arg: any, options: types.WaitForFunctionOptions = {}): Promise<js.SmartHandle<R>> {
async _waitForFunctionExpression<R>(metadata: CallMetadata, expression: string, isFunction: boolean | undefined, arg: any, options: types.WaitForFunctionOptions = {}): Promise<js.SmartHandle<R>> {
const controller = new ProgressController(metadata, this);
if (typeof options.pollingInterval === 'number')
assert(options.pollingInterval > 0, 'Cannot poll with non-positive interval: ' + options.pollingInterval);
expression = js.normalizeEvaluationExpression(expression, isFunction);
@ -1038,7 +1064,7 @@ export class Frame extends SdkObject {
return injectedScript.pollRaf((progress, continuePolling) => predicate(arg) || continuePolling);
return injectedScript.pollInterval(polling, (progress, continuePolling) => predicate(arg) || continuePolling);
}, { expression, isFunction, polling: options.pollingInterval, arg });
return runAbortableTask(
return controller.run(
progress => this._scheduleRerunnableHandleTask(progress, 'main', task),
this._page._timeoutSettings.timeout(options));
}

View File

@ -0,0 +1,114 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter } from 'events';
import type { Browser } from './browser';
import type { BrowserContext } from './browserContext';
import type { BrowserType } from './browserType';
import type { Frame } from './frames';
import type { Page } from './page';
export type Attribution = {
browserType?: BrowserType;
browser?: Browser;
context?: BrowserContext;
page?: Page;
frame?: Frame;
};
export type CallMetadata = {
type: string;
method: string;
params: any;
stack: string;
};
export class SdkObject extends EventEmitter {
attribution: Attribution;
instrumentation: Instrumentation;
protected constructor(parent: SdkObject) {
super();
this.setMaxListeners(0);
this.attribution = { ...parent.attribution };
this.instrumentation = parent.instrumentation;
}
}
export type ActionResult = {
logs: string[],
startTime: number,
endTime: number,
error?: Error,
};
export interface Instrumentation {
onContextCreated(context: BrowserContext): Promise<void>;
onContextWillDestroy(context: BrowserContext): Promise<void>;
onContextDidDestroy(context: BrowserContext): Promise<void>;
onActionCheckpoint(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onAfterAction(result: ActionResult, sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
}
export interface InstrumentationListener {
onContextCreated?(context: BrowserContext): Promise<void>;
onContextWillDestroy?(context: BrowserContext): Promise<void>;
onContextDidDestroy?(context: BrowserContext): Promise<void>;
onActionCheckpoint?(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onAfterAction?(result: ActionResult, sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
}
export class InstrumentationMultiplexer implements Instrumentation {
private _listeners: InstrumentationListener[];
constructor(listeners: InstrumentationListener[]) {
this._listeners = listeners;
}
async onContextCreated(context: BrowserContext): Promise<void> {
for (const listener of this._listeners)
await listener.onContextCreated?.(context);
}
async onContextWillDestroy(context: BrowserContext): Promise<void> {
for (const listener of this._listeners)
await listener.onContextWillDestroy?.(context);
}
async onContextDidDestroy(context: BrowserContext): Promise<void> {
for (const listener of this._listeners)
await listener.onContextDidDestroy?.(context);
}
async onActionCheckpoint(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
for (const listener of this._listeners)
await listener.onActionCheckpoint?.(name, sdkObject, metadata);
}
async onAfterAction(result: ActionResult, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
for (const listener of this._listeners)
await listener.onAfterAction?.(result, sdkObject, metadata);
}
}
export function internalCallMetadata(): CallMetadata {
return {
type: 'Internal',
method: '',
params: {},
stack: ''
};
}

View File

@ -18,7 +18,7 @@ import * as dom from './dom';
import * as utilityScriptSource from '../generated/utilityScriptSource';
import { serializeAsCallArgument } from './common/utilityScriptSerializers';
import type UtilityScript from './injected/utilityScript';
import { SdkObject } from './sdkObject';
import { SdkObject } from './instrumentation';
type ObjectId = string;
export type RemoteObject = {

View File

@ -17,7 +17,7 @@
import * as frames from './frames';
import * as types from './types';
import { assert } from '../utils/utils';
import { SdkObject } from './sdkObject';
import { SdkObject } from './instrumentation';
export function filterCookies(cookies: types.NetworkCookie[], urls: string[]): types.NetworkCookie[] {
const parsedURLs = urls.map(s => new URL(s));

View File

@ -27,11 +27,11 @@ import { BrowserContext, Video } from './browserContext';
import { ConsoleMessage } from './console';
import * as accessibility from './accessibility';
import { FileChooser } from './fileChooser';
import { ProgressController, runAbortableTask } from './progress';
import { ProgressController } from './progress';
import { assert, isError } from '../utils/utils';
import { debugLogger } from '../utils/debugLogger';
import { Selectors } from './selectors';
import { SdkObject } from './sdkObject';
import { CallMetadata, SdkObject } from './instrumentation';
export interface PageDelegate {
readonly rawMouse: input.RawMouse;
@ -288,7 +288,7 @@ export class Page extends SdkObject {
}
_addConsoleMessage(type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text?: string) {
const message = new ConsoleMessage(this, type, text, args, location);
const message = new ConsoleMessage(type, text, args, location);
const intercepted = this._frameManager.interceptConsoleMessage(message);
if (intercepted || !this.listenerCount(Page.Events.Console))
args.forEach(arg => arg.dispose());
@ -296,8 +296,8 @@ export class Page extends SdkObject {
this.emit(Page.Events.Console, message);
}
async reload(controller: ProgressController, options: types.NavigateOptions): Promise<network.Response | null> {
this.mainFrame().setupNavigationProgressController(controller);
async reload(metadata: CallMetadata, options: types.NavigateOptions): Promise<network.Response | null> {
const controller = this.mainFrame().setupNavigationProgressController(metadata);
const response = await controller.run(async progress => {
// Note: waitForNavigation may fail before we get response to reload(),
// so we should await it immediately.
@ -311,8 +311,8 @@ export class Page extends SdkObject {
return response;
}
async goBack(controller: ProgressController, options: types.NavigateOptions): Promise<network.Response | null> {
this.mainFrame().setupNavigationProgressController(controller);
async goBack(metadata: CallMetadata, options: types.NavigateOptions): Promise<network.Response | null> {
const controller = this.mainFrame().setupNavigationProgressController(metadata);
const response = await controller.run(async progress => {
// Note: waitForNavigation may fail before we get response to goBack,
// so we should catch it immediately.
@ -333,8 +333,8 @@ export class Page extends SdkObject {
return response;
}
async goForward(controller: ProgressController, options: types.NavigateOptions): Promise<network.Response | null> {
this.mainFrame().setupNavigationProgressController(controller);
async goForward(metadata: CallMetadata, options: types.NavigateOptions): Promise<network.Response | null> {
const controller = this.mainFrame().setupNavigationProgressController(metadata);
const response = await controller.run(async progress => {
// Note: waitForNavigation may fail before we get response to goForward,
// so we should catch it immediately.
@ -421,8 +421,9 @@ export class Page extends SdkObject {
route.continue();
}
async screenshot(options: types.ScreenshotOptions = {}): Promise<Buffer> {
return runAbortableTask(
async screenshot(metadata: CallMetadata, options: types.ScreenshotOptions = {}): Promise<Buffer> {
const controller = new ProgressController(metadata, this);
return controller.run(
progress => this._screenshotter.screenshotPage(progress, options),
this._timeoutSettings.timeout(options));
}

View File

@ -22,12 +22,12 @@ import { PlaywrightOptions } from './browser';
import { Chromium } from './chromium/chromium';
import { Electron } from './electron/electron';
import { Firefox } from './firefox/firefox';
import { Selectors } from './selectors';
import { Selectors, serverSelectors } from './selectors';
import { HarTracer } from './supplements/har/harTracer';
import { InspectorController } from './supplements/inspectorController';
import { WebKit } from './webkit/webkit';
import { Registry } from '../utils/registry';
import { SdkObject } from './sdkObject';
import { InstrumentationMultiplexer, SdkObject } from './instrumentation';
export class Playwright extends SdkObject {
readonly selectors: Selectors;
@ -39,25 +39,23 @@ export class Playwright extends SdkObject {
readonly options: PlaywrightOptions;
constructor(isInternal: boolean) {
super(null);
this.selectors = new Selectors(this);
const instrumentation = new InstrumentationMultiplexer(isInternal ? [] : [
new InspectorController(),
new Tracer(),
new HarTracer()
]);
super({ attribution: {}, instrumentation } as any);
this.options = {
isInternal,
registry: new Registry(path.join(__dirname, '..', '..')),
contextListeners: isInternal ? [] : [
new InspectorController(),
new Tracer(),
new HarTracer()
],
rootSdkObject: this,
selectors: this.selectors
};
this.chromium = new Chromium(this.options);
this.firefox = new Firefox(this.options);
this.webkit = new WebKit(this.options);
this.electron = new Electron(this.options);
this.android = new Android(new AdbBackend(), this.options);
this.selectors = serverSelectors;
}
}

View File

@ -18,13 +18,7 @@ import { TimeoutError } from '../utils/errors';
import { assert, monotonicTime } from '../utils/utils';
import { rewriteErrorMessage } from '../utils/stackTrace';
import { debugLogger, LogName } from '../utils/debugLogger';
export type ProgressResult = {
logs: string[],
startTime: number,
endTime: number,
error?: Error,
};
import { CallMetadata, Instrumentation, SdkObject } from './instrumentation';
export interface Progress {
log(message: string): void;
@ -35,16 +29,6 @@ export interface Progress {
checkpoint(name: string): Promise<void>;
}
export interface ProgressListener {
onProgressCheckpoint(name: string): Promise<void>;
onProgressDone(result: ProgressResult): Promise<void>;
}
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 {
// Promise and callback that forcefully abort the progress.
// This promise always rejects.
@ -59,21 +43,22 @@ export class ProgressController {
private _deadline: number = 0;
private _timeout: number = 0;
private _logRecording: string[] = [];
private _listener?: ProgressListener;
readonly metadata: CallMetadata;
readonly instrumentation: Instrumentation;
readonly sdkObject: SdkObject;
constructor() {
constructor(metadata: CallMetadata, sdkObject: SdkObject) {
this.metadata = metadata;
this.sdkObject = sdkObject;
this.instrumentation = sdkObject.instrumentation;
this._forceAbortPromise = new Promise((resolve, reject) => this._forceAbort = reject);
this._forceAbortPromise.catch(e => null); // Prevent unhandle promsie rejection.
this._forceAbortPromise.catch(e => null); // Prevent unhandled promise rejection.
}
setLogName(logName: LogName) {
this._logName = logName;
}
setListener(listener: ProgressListener) {
this._listener = listener;
}
async run<T>(task: (progress: Progress) => Promise<T>, timeout?: number): Promise<T> {
if (timeout) {
this._timeout = timeout;
@ -102,8 +87,7 @@ export class ProgressController {
throw new AbortedError();
},
checkpoint: async (name: string) => {
if (this._listener)
await this._listener.onProgressCheckpoint(name);
await this.instrumentation.onActionCheckpoint(name, this.sdkObject, this.metadata);
},
};
@ -115,27 +99,23 @@ export class ProgressController {
const result = await Promise.race([promise, this._forceAbortPromise]);
clearTimeout(timer);
this._state = 'finished';
if (this._listener) {
await this._listener.onProgressDone({
startTime,
endTime: monotonicTime(),
logs: this._logRecording,
});
}
await this.instrumentation.onAfterAction({
startTime,
endTime: monotonicTime(),
logs: this._logRecording,
}, this.sdkObject, this.metadata);
this._logRecording = [];
return result;
} catch (e) {
clearTimeout(timer);
this._state = 'aborted';
await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(cleanup)));
if (this._listener) {
await this._listener.onProgressDone({
startTime,
endTime: monotonicTime(),
logs: this._logRecording,
error: e,
});
}
await this.instrumentation.onAfterAction({
startTime,
endTime: monotonicTime(),
logs: this._logRecording,
error: e,
}, this.sdkObject, this.metadata);
rewriteErrorMessage(e,
e.message +
formatLogRecording(this._logRecording) +

View File

@ -1,39 +0,0 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter } from 'events';
import type { Browser } from './browser';
import type { BrowserContext } from './browserContext';
import type { BrowserType } from './browserType';
import type { Frame } from './frames';
import type { Page } from './page';
export type Attribution = {
browserType?: BrowserType;
browser?: Browser;
context?: BrowserContext;
page?: Page;
frame?: Frame;
};
export class SdkObject extends EventEmitter {
attribution: Attribution;
constructor(parent: SdkObject | null) {
super();
this.setMaxListeners(0);
this.attribution = { ...parent?.attribution };
}
}

View File

@ -19,7 +19,6 @@ import * as frames from './frames';
import * as js from './javascript';
import * as types from './types';
import { ParsedSelector, parseSelector } from './common/selectorParser';
import { SdkObject } from './sdkObject';
export type SelectorInfo = {
parsed: ParsedSelector,
@ -27,12 +26,11 @@ export type SelectorInfo = {
selector: string,
};
export class Selectors extends SdkObject {
export class Selectors {
readonly _builtinEngines: Set<string>;
readonly _engines: Map<string, { source: string, contentScript: boolean }>;
constructor(parent: SdkObject) {
super(parent);
constructor() {
// Note: keep in sync with InjectedScript class.
this._builtinEngines = new Set([
'css', 'css:light',
@ -136,6 +134,4 @@ export class Selectors extends SdkObject {
}
}
export function serverSelectors(parent: SdkObject) {
return new Selectors(parent);
}
export const serverSelectors = new Selectors();

View File

@ -16,15 +16,16 @@
import * as fs from 'fs';
import * as util from 'util';
import { BrowserContext, ContextListener } from '../../browserContext';
import { BrowserContext } from '../../browserContext';
import { helper } from '../../helper';
import * as network from '../../network';
import { Page } from '../../page';
import { InstrumentationListener } from '../../instrumentation';
import * as har from './har';
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
export class HarTracer implements ContextListener {
export class HarTracer implements InstrumentationListener {
private _contextTracers = new Map<BrowserContext, HarContextTracer>();
async onContextCreated(context: BrowserContext): Promise<void> {

View File

@ -14,11 +14,12 @@
* limitations under the License.
*/
import { BrowserContext, ContextListener } from '../browserContext';
import { BrowserContext } from '../browserContext';
import { isDebugMode } from '../../utils/utils';
import { RecorderSupplement } from './recorderSupplement';
import { InstrumentationListener } from '../instrumentation';
export class InspectorController implements ContextListener {
export class InspectorController implements InstrumentationListener {
async onContextCreated(context: BrowserContext): Promise<void> {
if (isDebugMode()) {
RecorderSupplement.getOrCreate(context, {

View File

@ -22,6 +22,7 @@ import { Page } from '../../page';
import { ProgressController } from '../../progress';
import { createPlaywright } from '../../playwright';
import { EventEmitter } from 'events';
import { internalCallMetadata } from '../../instrumentation';
const readFileAsync = util.promisify(fs.readFile);
@ -85,12 +86,13 @@ export class RecorderApp extends EventEmitter {
this._page.context().close().catch(e => console.error(e));
});
await this._page.mainFrame().goto(new ProgressController(), 'https://playwright/index.html');
const mainFrame = this._page.mainFrame();
await mainFrame.goto(internalCallMetadata(), 'https://playwright/index.html');
}
static async open(): Promise<RecorderApp> {
const recorderPlaywright = createPlaywright(true);
const context = await recorderPlaywright.chromium.launchPersistentContext(undefined, {
const context = await recorderPlaywright.chromium.launchPersistentContext(internalCallMetadata(), undefined, {
args: [
'--app=data:text/html,',
'--window-size=300,800',
@ -98,7 +100,7 @@ export class RecorderApp extends EventEmitter {
noDefaultViewport: true
});
const controller = new ProgressController();
const controller = new ProgressController(internalCallMetadata(), context._browser);
await controller.run(async progress => {
await context._browser._defaultContext!._loadDefaultContextAsIs(progress);
});

View File

@ -25,11 +25,11 @@ import { LanguageGenerator } from './recorder/language';
import { JavaScriptLanguageGenerator } from './recorder/javascript';
import { CSharpLanguageGenerator } from './recorder/csharp';
import { PythonLanguageGenerator } from './recorder/python';
import { ProgressController } from '../progress';
import * as recorderSource from '../../generated/recorderSource';
import * as consoleApiSource from '../../generated/consoleApiSource';
import { BufferedOutput, FileOutput, FlushingTerminalOutput, OutputMultiplexer, RecorderOutput, TerminalOutput, Writable } from './recorder/outputs';
import { EventData, Mode, RecorderApp } from './recorder/recorderApp';
import { internalCallMetadata } from '../instrumentation';
type BindingSource = { frame: Frame, page: Page };
@ -238,30 +238,30 @@ export class RecorderSupplement {
private async _performAction(frame: Frame, action: actions.Action) {
const page = frame._page;
const controller = new ProgressController();
const actionInContext: ActionInContext = {
pageAlias: this._pageAliases.get(page)!,
...describeFrame(frame),
action
};
this._generator.willPerformAction(actionInContext);
const noCallMetadata = internalCallMetadata();
try {
const kActionTimeout = 5000;
if (action.name === 'click') {
const { options } = toClickOptions(action);
await frame.click(controller, action.selector, { ...options, timeout: kActionTimeout });
await frame.click(noCallMetadata, action.selector, { ...options, timeout: kActionTimeout });
}
if (action.name === 'press') {
const modifiers = toModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+');
await frame.press(controller, action.selector, shortcut, { timeout: kActionTimeout });
await frame.press(noCallMetadata, action.selector, shortcut, { timeout: kActionTimeout });
}
if (action.name === 'check')
await frame.check(controller, action.selector, { timeout: kActionTimeout });
await frame.check(noCallMetadata, action.selector, { timeout: kActionTimeout });
if (action.name === 'uncheck')
await frame.uncheck(controller, action.selector, { timeout: kActionTimeout });
await frame.uncheck(noCallMetadata, action.selector, { timeout: kActionTimeout });
if (action.name === 'select')
await frame.selectOption(controller, action.selector, [], action.options.map(value => ({ value })), { timeout: kActionTimeout });
await frame.selectOption(noCallMetadata, action.selector, [], action.options.map(value => ({ value })), { timeout: kActionTimeout });
} catch (e) {
this._generator.performedActionFailed(actionInContext);
return;

View File

@ -78,15 +78,14 @@ export type ActionTraceEvent = {
timestamp: number,
type: 'action',
contextId: string,
action: string,
objectType: string,
method: string,
params: any,
stack?: string,
pageId?: string,
selector?: string,
label?: string,
value?: string,
startTime: number,
endTime: number,
logs?: string[],
stack?: string,
error?: string,
snapshots?: { name: string, snapshotId: string }[],
};

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { ActionListener, ActionMetadata, BrowserContext, ContextListener, Video } from '../server/browserContext';
import { BrowserContext, Video } from '../server/browserContext';
import type { SnapshotterResource as SnapshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
import * as trace from './traceTypes';
import * as path from 'path';
@ -24,17 +24,17 @@ import { createGuid, getFromENV, mkdirIfNeeded, monotonicTime } from '../utils/u
import { Page } from '../server/page';
import { Snapshotter } from './snapshotter';
import { helper, RegisteredListener } from '../server/helper';
import { ProgressResult } from '../server/progress';
import { Dialog } from '../server/dialog';
import { Frame, NavigationEvent } from '../server/frames';
import { snapshotScript } from './snapshotterInjected';
import { ActionResult, CallMetadata, InstrumentationListener, SdkObject } from '../server/instrumentation';
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs));
const fsAccessAsync = util.promisify(fs.access.bind(fs));
const envTrace = getFromENV('PW_TRACE_DIR');
export class Tracer implements ContextListener {
export class Tracer implements InstrumentationListener {
private _contextTracers = new Map<BrowserContext, ContextTracer>();
async onContextCreated(context: BrowserContext): Promise<void> {
@ -56,19 +56,27 @@ export class Tracer implements ContextListener {
this._contextTracers.delete(context);
}
}
async onActionCheckpoint(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
this._contextTracers.get(sdkObject.attribution.context!)?.onActionCheckpoint(name, sdkObject, metadata);
}
async onAfterAction(result: ActionResult, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
this._contextTracers.get(sdkObject.attribution.context!)?.onAfterAction(result, sdkObject, metadata);
}
}
const pageIdSymbol = Symbol('pageId');
const snapshotsSymbol = Symbol('snapshots');
// TODO: this is a hacky way to pass snapshots between onActionCheckpoint and onAfterAction.
function snapshotsForMetadata(metadata: ActionMetadata): { name: string, snapshotId: string }[] {
function snapshotsForMetadata(metadata: CallMetadata): { name: string, snapshotId: string }[] {
if (!(metadata as any)[snapshotsSymbol])
(metadata as any)[snapshotsSymbol] = [];
return (metadata as any)[snapshotsSymbol];
}
class ContextTracer implements SnapshotterDelegate, ActionListener {
class ContextTracer implements SnapshotterDelegate {
private _context: BrowserContext;
private _contextId: string;
private _traceStoragePromise: Promise<string>;
@ -102,7 +110,6 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
this._eventListeners = [
helper.addEventListener(context, BrowserContext.Events.Page, this._onPage.bind(this)),
];
this._context._actionListeners.add(this);
}
onBlob(blob: SnapshotterBlob): void {
@ -147,24 +154,29 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
return (page as any)[pageIdSymbol];
}
async onActionCheckpoint(name: string, metadata: ActionMetadata): Promise<void> {
async onActionCheckpoint(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (!sdkObject.attribution.page)
return;
const snapshotId = createGuid();
snapshotsForMetadata(metadata).push({ name, snapshotId });
await this._snapshotter.forceSnapshot(metadata.page, snapshotId);
await this._snapshotter.forceSnapshot(sdkObject.attribution.page, snapshotId);
}
async onAfterAction(result: ProgressResult, metadata: ActionMetadata): Promise<void> {
async onAfterAction(result: ActionResult, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (!sdkObject.attribution.page)
return;
const event: trace.ActionTraceEvent = {
timestamp: monotonicTime(),
type: 'action',
contextId: this._contextId,
pageId: this.pageId(metadata.page),
action: metadata.type,
selector: typeof metadata.target === 'string' ? metadata.target : undefined,
value: metadata.value,
pageId: this.pageId(sdkObject.attribution.page),
objectType: metadata.type,
method: metadata.method,
// FIXME: filter out evaluation snippets, binary
params: metadata.params,
stack: metadata.stack,
startTime: result.startTime,
endTime: result.endTime,
stack: metadata.stack,
logs: result.logs.slice(),
error: result.error ? result.error.stack : undefined,
snapshots: snapshotsForMetadata(metadata),
@ -265,7 +277,6 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
async dispose() {
this._disposed = true;
this._context._actionListeners.delete(this);
helper.removeEventListeners(this._eventListeners);
this._snapshotter.dispose();
const event: trace.ContextDestroyedTraceEvent = {

View File

@ -36,6 +36,7 @@ export const ActionList: React.FC<ActionListProps> = ({
const targetAction = highlightedAction || selectedAction;
return <div className='action-list'>{actions.map(actionEntry => {
const { action, actionId, thumbnailUrl } = actionEntry;
const selector = action.params.selector;
return <div
className={'action-entry' + (actionEntry === targetAction ? ' selected' : '')}
key={actionId}
@ -45,9 +46,9 @@ export const ActionList: React.FC<ActionListProps> = ({
>
<div className='action-header'>
<div className={'action-error codicon codicon-issues'} hidden={!actionEntry.action.error} />
<div className='action-title'>{action.action}</div>
{action.selector && <div className='action-selector' title={action.selector}>{action.selector}</div>}
{action.action === 'goto' && action.value && <div className='action-url' title={action.value}>{action.value}</div>}
<div className='action-title'>{action.method}</div>
{action.params.selector && <div className='action-selector' title={action.params.selector}>{action.params.selector}</div>}
{action.method === 'goto' && action.params.url && <div className='action-url' title={action.params.url}>{action.params.url}</div>}
</div>
<div className='action-thumbnail'>
<img src={thumbnailUrl} />

View File

@ -54,17 +54,17 @@ export const Timeline: React.FunctionComponent<{
const bars: TimelineBar[] = [];
for (const page of context.pages) {
for (const entry of page.actions) {
let detail = entry.action.selector || '';
if (entry.action.action === 'goto')
detail = entry.action.value || '';
let detail = entry.action.params.selector || '';
if (entry.action.method === 'goto')
detail = entry.action.params.url || '';
bars.push({
entry,
leftTime: entry.action.startTime,
rightTime: entry.action.endTime,
leftPosition: timeToPosition(measure.width, boundaries, entry.action.startTime),
rightPosition: timeToPosition(measure.width, boundaries, entry.action.endTime),
label: entry.action.action + ' ' + detail,
type: entry.action.action,
label: entry.action.method + ' ' + detail,
type: entry.action.method,
priority: 0,
});
if (entry === (highlightedAction || selectedAction))

View File

@ -15,7 +15,7 @@
*/
import { folio } from './fixtures';
import { ProgressController } from '../lib/server/progress';
import { internalCallMetadata } from '../lib/server/instrumentation';
const extended = folio.extend<{
recorderFrame: () => Promise<any>,
@ -40,7 +40,7 @@ extended.recorderFrame.init(async ({context, toImpl}, runTest) => {
extended.recorderClick.init(async ({ recorderFrame }, runTest) => {
await runTest(async (selector: string) => {
const frame = await recorderFrame();
frame.click(new ProgressController(), selector, {});
frame.click(internalCallMetadata(), selector, {});
});
});

View File

@ -43,11 +43,11 @@ it('should record trace', (test, { browserName, platform }) => {
expect(pageEvent.contextId).toBe(contextId);
const pageId = pageEvent.pageId;
const gotoEvent = traceEvents.find(event => event.type === 'action' && event.action === 'goto') as trace.ActionTraceEvent;
const gotoEvent = traceEvents.find(event => event.type === 'action' && event.method === 'goto') as trace.ActionTraceEvent;
expect(gotoEvent).toBeTruthy();
expect(gotoEvent.contextId).toBe(contextId);
expect(gotoEvent.pageId).toBe(pageId);
expect(gotoEvent.value).toBe(url);
expect(gotoEvent.params.url).toBe(url);
const resourceEvent = traceEvents.find(event => event.type === 'resource' && event.url.endsWith('/frames/style.css')) as trace.NetworkResourceTraceEvent;
expect(resourceEvent).toBeTruthy();
@ -59,7 +59,7 @@ it('should record trace', (test, { browserName, platform }) => {
expect(resourceEvent.requestHeaders.length).toBeGreaterThan(0);
expect(resourceEvent.requestSha1).toBe('none');
const clickEvent = traceEvents.find(event => event.type === 'action' && event.action === 'click') as trace.ActionTraceEvent;
const clickEvent = traceEvents.find(event => event.type === 'action' && event.method === 'click') as trace.ActionTraceEvent;
expect(clickEvent).toBeTruthy();
expect(clickEvent.snapshots.length).toBe(2);
const snapshotId = clickEvent.snapshots[0].snapshotId;
@ -93,11 +93,11 @@ it('should record trace with POST', (test, { browserName, platform }) => {
expect(pageEvent.contextId).toBe(contextId);
const pageId = pageEvent.pageId;
const gotoEvent = traceEvents.find(event => event.type === 'action' && event.action === 'goto') as trace.ActionTraceEvent;
const gotoEvent = traceEvents.find(event => event.type === 'action' && event.method === 'goto') as trace.ActionTraceEvent;
expect(gotoEvent).toBeTruthy();
expect(gotoEvent.contextId).toBe(contextId);
expect(gotoEvent.pageId).toBe(pageId);
expect(gotoEvent.value).toBe(url);
expect(gotoEvent.params.url).toBe(url);
const resourceEvent = traceEvents.find(event => event.type === 'resource' && event.url.endsWith('/file.json')) as trace.NetworkResourceTraceEvent;
expect(resourceEvent).toBeTruthy();