feat(tracing): make context.request appear in the trace (#9555)
This commit is contained in:
parent
4ce765c3ae
commit
e31b96cc26
|
|
@ -238,7 +238,7 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel, c
|
||||||
return this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => {
|
return this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => {
|
||||||
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
||||||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||||
const waiter = Waiter.createForEvent(this, event);
|
const waiter = Waiter.createForEvent(channel, event);
|
||||||
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
|
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
|
||||||
if (event !== Events.AndroidDevice.Close)
|
if (event !== Events.AndroidDevice.Close)
|
||||||
waiter.rejectOnEvent(this, Events.AndroidDevice.Close, new Error('Device closed'));
|
waiter.rejectOnEvent(this, Events.AndroidDevice.Close, new Error('Device closed'));
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
||||||
}
|
}
|
||||||
if (!handled) {
|
if (!handled) {
|
||||||
// it can race with BrowserContext.close() which then throws since its closed
|
// it can race with BrowserContext.close() which then throws since its closed
|
||||||
route.continue().catch(() => {});
|
route._internalContinue();
|
||||||
} else {
|
} else {
|
||||||
this._routes = this._routes.filter(route => !route.expired());
|
this._routes = this._routes.filter(route => !route.expired());
|
||||||
}
|
}
|
||||||
|
|
@ -293,7 +293,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
||||||
return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
|
return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
|
||||||
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
||||||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||||
const waiter = Waiter.createForEvent(this, event);
|
const waiter = Waiter.createForEvent(channel, event);
|
||||||
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
|
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
|
||||||
if (event !== Events.BrowserContext.Close)
|
if (event !== Events.BrowserContext.Close)
|
||||||
waiter.rejectOnEvent(this, Events.BrowserContext.Close, new Error('Context closed'));
|
waiter.rejectOnEvent(this, Events.BrowserContext.Close, new Error('Context closed'));
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
||||||
if (validator) {
|
if (validator) {
|
||||||
return (params: any) => {
|
return (params: any) => {
|
||||||
if (callCookie && csi) {
|
if (callCookie && csi) {
|
||||||
callCookie.userObject = csi.onApiCallBegin(renderCallWithParams(stackTrace!.apiName, params)).userObject;
|
callCookie.userObject = csi.onApiCallBegin(renderCallWithParams(stackTrace!.apiName!, params)).userObject;
|
||||||
csi = undefined;
|
csi = undefined;
|
||||||
}
|
}
|
||||||
return this._connection.sendMessageToServer(this, prop, validator(params, ''), stackTrace);
|
return this._connection.sendMessageToServer(this, prop, validator(params, ''), stackTrace);
|
||||||
|
|
@ -96,7 +96,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
||||||
return channel;
|
return channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _wrapApiCall<R, C extends channels.Channel = T>(func: (channel: C, stackTrace: ParsedStackTrace) => Promise<R>, logger?: Logger): Promise<R> {
|
async _wrapApiCall<R, C extends channels.Channel = T>(func: (channel: C, stackTrace: ParsedStackTrace) => Promise<R>, logger?: Logger, isInternal?: boolean): Promise<R> {
|
||||||
logger = logger || this._logger;
|
logger = logger || this._logger;
|
||||||
const stackTrace = captureStackTrace();
|
const stackTrace = captureStackTrace();
|
||||||
const { apiName, frameTexts } = stackTrace;
|
const { apiName, frameTexts } = stackTrace;
|
||||||
|
|
@ -106,23 +106,25 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
||||||
ancestorWithCSI = ancestorWithCSI._parent;
|
ancestorWithCSI = ancestorWithCSI._parent;
|
||||||
|
|
||||||
// Do not report nested async calls to _wrapApiCall.
|
// Do not report nested async calls to _wrapApiCall.
|
||||||
const isNested = stackTrace.allFrames.filter(f => f.function?.includes('_wrapApiCall')).length > 1;
|
isInternal = isInternal || stackTrace.allFrames.filter(f => f.function?.includes('_wrapApiCall')).length > 1;
|
||||||
const csi = isNested ? undefined : ancestorWithCSI._csi;
|
if (isInternal)
|
||||||
|
delete stackTrace.apiName;
|
||||||
|
const csi = isInternal ? undefined : ancestorWithCSI._csi;
|
||||||
const callCookie: { userObject: any } = { userObject: null };
|
const callCookie: { userObject: any } = { userObject: null };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logApiCall(logger, `=> ${apiName} started`, isNested);
|
logApiCall(logger, `=> ${apiName} started`, isInternal);
|
||||||
const channel = this._createChannel({}, stackTrace, csi, callCookie);
|
const channel = this._createChannel({}, stackTrace, csi, callCookie);
|
||||||
const result = await func(channel as any, stackTrace);
|
const result = await func(channel as any, stackTrace);
|
||||||
csi?.onApiCallEnd(callCookie);
|
csi?.onApiCallEnd(callCookie);
|
||||||
logApiCall(logger, `<= ${apiName} succeeded`, isNested);
|
logApiCall(logger, `<= ${apiName} succeeded`, isInternal);
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const innerError = ((process.env.PWDEBUGIMPL || isUnderTest()) && e.stack) ? '\n<inner error>\n' + e.stack : '';
|
const innerError = ((process.env.PWDEBUGIMPL || isUnderTest()) && e.stack) ? '\n<inner error>\n' + e.stack : '';
|
||||||
e.message = apiName + ': ' + e.message;
|
e.message = apiName + ': ' + e.message;
|
||||||
e.stack = e.message + '\n' + frameTexts.join('\n') + innerError;
|
e.stack = e.message + '\n' + frameTexts.join('\n') + innerError;
|
||||||
csi?.onApiCallEnd(callCookie, e);
|
csi?.onApiCallEnd(callCookie, e);
|
||||||
logApiCall(logger, `<= ${apiName} failed`, isNested);
|
logApiCall(logger, `<= ${apiName} failed`, isInternal);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
|
||||||
return this._wrapApiCall(async (channel: channels.ElectronApplicationChannel) => {
|
return this._wrapApiCall(async (channel: channels.ElectronApplicationChannel) => {
|
||||||
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
||||||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||||
const waiter = Waiter.createForEvent(this, event);
|
const waiter = Waiter.createForEvent(channel, event);
|
||||||
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
|
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
|
||||||
if (event !== Events.ElectronApplication.Close)
|
if (event !== Events.ElectronApplication.Close)
|
||||||
waiter.rejectOnEvent(this, Events.ElectronApplication.Close, new Error('Electron application closed'));
|
waiter.rejectOnEvent(this, Events.ElectronApplication.Close, new Error('Electron application closed'));
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,8 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setupNavigationWaiter(options: { timeout?: number }): Waiter {
|
private _setupNavigationWaiter(channel: channels.EventTargetChannel, options: { timeout?: number }): Waiter {
|
||||||
const waiter = new Waiter(this._page!, '');
|
const waiter = new Waiter(channel, '');
|
||||||
if (this._page!.isClosed())
|
if (this._page!.isClosed())
|
||||||
waiter.rejectImmediately(new Error('Navigation failed because page was closed!'));
|
waiter.rejectImmediately(new Error('Navigation failed because page was closed!'));
|
||||||
waiter.rejectOnEvent(this._page!, Events.Page.Close, new Error('Navigation failed because page was closed!'));
|
waiter.rejectOnEvent(this._page!, Events.Page.Close, new Error('Navigation failed because page was closed!'));
|
||||||
|
|
@ -107,9 +107,9 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForNavigation(options: WaitForNavigationOptions = {}): Promise<network.Response | null> {
|
async waitForNavigation(options: WaitForNavigationOptions = {}): Promise<network.Response | null> {
|
||||||
return this._wrapApiCall(async (channel: channels.FrameChannel) => {
|
return this._page!._wrapApiCall(async (channel: channels.PageChannel) => {
|
||||||
const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
|
const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
|
||||||
const waiter = this._setupNavigationWaiter(options);
|
const waiter = this._setupNavigationWaiter(channel, options);
|
||||||
|
|
||||||
const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : '';
|
const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : '';
|
||||||
waiter.log(`waiting for navigation${toUrl} until "${waitUntil}"`);
|
waiter.log(`waiting for navigation${toUrl} until "${waitUntil}"`);
|
||||||
|
|
@ -135,7 +135,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = navigatedEvent.newDocument ? network.Request.fromNullable(navigatedEvent.newDocument.request) : null;
|
const request = navigatedEvent.newDocument ? network.Request.fromNullable(navigatedEvent.newDocument.request) : null;
|
||||||
const response = request ? await waiter.waitForPromise(request._finalRequest().response()) : null;
|
const response = request ? await waiter.waitForPromise(request._finalRequest()._internalResponse()) : null;
|
||||||
waiter.dispose();
|
waiter.dispose();
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
|
|
@ -145,8 +145,8 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
|
||||||
state = verifyLoadState('state', state);
|
state = verifyLoadState('state', state);
|
||||||
if (this._loadStates.has(state))
|
if (this._loadStates.has(state))
|
||||||
return;
|
return;
|
||||||
return this._wrapApiCall(async (channel: channels.FrameChannel) => {
|
return this._page!._wrapApiCall(async (channel: channels.PageChannel) => {
|
||||||
const waiter = this._setupNavigationWaiter(options);
|
const waiter = this._setupNavigationWaiter(channel, options);
|
||||||
await waiter.waitForEvent<LifecycleEvent>(this._eventEmitter, 'loadstate', s => {
|
await waiter.waitForEvent<LifecycleEvent>(this._eventEmitter, 'loadstate', s => {
|
||||||
waiter.log(` "${s}" event fired`);
|
waiter.log(` "${s}" event fired`);
|
||||||
return s === state;
|
return s === state;
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,12 @@ export class Request extends ChannelOwner<channels.RequestChannel, channels.Requ
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _internalResponse(): Promise<Response | null> {
|
||||||
|
return this._wrapApiCall(async (channel: channels.RequestChannel) => {
|
||||||
|
return Response.fromNullable((await channel.response()).response);
|
||||||
|
}, undefined, true);
|
||||||
|
}
|
||||||
|
|
||||||
frame(): Frame {
|
frame(): Frame {
|
||||||
return Frame.from(this._initializer.frame);
|
return Frame.from(this._initializer.frame);
|
||||||
}
|
}
|
||||||
|
|
@ -386,9 +392,13 @@ export class Route extends ChannelOwner<channels.RouteChannel, channels.RouteIni
|
||||||
await this._continue(options, false);
|
await this._continue(options, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: NotInterceptResponse): Promise<null>;
|
async _internalContinue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer } = {}) {
|
||||||
async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: InterceptResponse): Promise<api.Response>;
|
await this._continue(options, false, true).catch(() => {});
|
||||||
async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: boolean): Promise<null|api.Response> {
|
}
|
||||||
|
|
||||||
|
async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: NotInterceptResponse, isInternal?: boolean): Promise<null>;
|
||||||
|
async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: InterceptResponse, isInternal?: boolean): Promise<api.Response>;
|
||||||
|
async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: boolean, isInternal?: boolean): Promise<null|api.Response> {
|
||||||
return await this._wrapApiCall(async (channel: channels.RouteChannel) => {
|
return await this._wrapApiCall(async (channel: channels.RouteChannel) => {
|
||||||
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
|
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
|
||||||
const result = await channel.continue({
|
const result = await channel.continue({
|
||||||
|
|
@ -401,7 +411,7 @@ export class Route extends ChannelOwner<channels.RouteChannel, channels.RouteIni
|
||||||
if (result.response)
|
if (result.response)
|
||||||
return new InterceptedResponse(this, result.response);
|
return new InterceptedResponse(this, result.response);
|
||||||
return null;
|
return null;
|
||||||
});
|
}, undefined, isInternal);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _responseBody(): Promise<Buffer> {
|
async _responseBody(): Promise<Buffer> {
|
||||||
|
|
@ -585,7 +595,7 @@ export class WebSocket extends ChannelOwner<channels.WebSocketChannel, channels.
|
||||||
return this._wrapApiCall(async (channel: channels.WebSocketChannel) => {
|
return this._wrapApiCall(async (channel: channels.WebSocketChannel) => {
|
||||||
const timeout = this._page._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
const timeout = this._page._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
||||||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||||
const waiter = Waiter.createForEvent(this, event);
|
const waiter = Waiter.createForEvent(channel, event);
|
||||||
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
|
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
|
||||||
if (event !== Events.WebSocket.Error)
|
if (event !== Events.WebSocket.Error)
|
||||||
waiter.rejectOnEvent(this, Events.WebSocket.Error, new Error('Socket error'));
|
waiter.rejectOnEvent(this, Events.WebSocket.Error, new Error('Socket error'));
|
||||||
|
|
|
||||||
|
|
@ -369,7 +369,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
|
||||||
};
|
};
|
||||||
const trimmedUrl = trimUrl(urlOrPredicate);
|
const trimmedUrl = trimUrl(urlOrPredicate);
|
||||||
const logLine = trimmedUrl ? `waiting for request ${trimmedUrl}` : undefined;
|
const logLine = trimmedUrl ? `waiting for request ${trimmedUrl}` : undefined;
|
||||||
return this._waitForEvent(Events.Page.Request, { predicate, timeout: options.timeout }, logLine);
|
return this._waitForEvent(channel, Events.Page.Request, { predicate, timeout: options.timeout }, logLine);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -382,18 +382,20 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
|
||||||
};
|
};
|
||||||
const trimmedUrl = trimUrl(urlOrPredicate);
|
const trimmedUrl = trimUrl(urlOrPredicate);
|
||||||
const logLine = trimmedUrl ? `waiting for response ${trimmedUrl}` : undefined;
|
const logLine = trimmedUrl ? `waiting for response ${trimmedUrl}` : undefined;
|
||||||
return this._waitForEvent(Events.Page.Response, { predicate, timeout: options.timeout }, logLine);
|
return this._waitForEvent(channel, Events.Page.Response, { predicate, timeout: options.timeout }, logLine);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
|
async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
|
||||||
return this._waitForEvent(event, optionsOrPredicate, `waiting for event "${event}"`);
|
return this._wrapApiCall(async channel => {
|
||||||
|
return this._waitForEvent(channel, event, optionsOrPredicate, `waiting for event "${event}"`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise<any> {
|
private async _waitForEvent(channel: channels.EventTargetChannel, event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise<any> {
|
||||||
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
||||||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||||
const waiter = Waiter.createForEvent(this, event);
|
const waiter = Waiter.createForEvent(channel, event);
|
||||||
if (logLine)
|
if (logLine)
|
||||||
waiter.log(logLine);
|
waiter.log(logLine);
|
||||||
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
|
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||||
import { TimeoutError } from '../utils/errors';
|
import { TimeoutError } from '../utils/errors';
|
||||||
import { createGuid } from '../utils/utils';
|
import { createGuid } from '../utils/utils';
|
||||||
import * as channels from '../protocol/channels';
|
import * as channels from '../protocol/channels';
|
||||||
import { ChannelOwner } from './channelOwner';
|
|
||||||
|
|
||||||
export class Waiter {
|
export class Waiter {
|
||||||
private _dispose: (() => void)[];
|
private _dispose: (() => void)[];
|
||||||
|
|
@ -31,19 +30,17 @@ export class Waiter {
|
||||||
private _waitId: string;
|
private _waitId: string;
|
||||||
private _error: string | undefined;
|
private _error: string | undefined;
|
||||||
|
|
||||||
constructor(channelOwner: ChannelOwner<channels.EventTargetChannel>, event: string) {
|
constructor(channel: channels.EventTargetChannel, event: string) {
|
||||||
this._waitId = createGuid();
|
this._waitId = createGuid();
|
||||||
this._channel = channelOwner._channel;
|
this._channel = channel;
|
||||||
channelOwner._wrapApiCall(async (channel: channels.EventTargetChannel) => {
|
this._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'before', event } }).catch(() => {});
|
||||||
channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'before', event } }).catch(() => {});
|
|
||||||
});
|
|
||||||
this._dispose = [
|
this._dispose = [
|
||||||
() => this._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'after', error: this._error } }).catch(() => {})
|
() => this._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'after', error: this._error } }).catch(() => {})
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
static createForEvent(channelOwner: ChannelOwner<channels.EventTargetChannel>, event: string) {
|
static createForEvent(channel: channels.EventTargetChannel, event: string) {
|
||||||
return new Waiter(channelOwner, event);
|
return new Waiter(channel, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForEvent<T = void>(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean | Promise<boolean>): Promise<T> {
|
async waitForEvent<T = void>(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean | Promise<boolean>): Promise<T> {
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,9 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
this._isPersistentContext = !browserContextId;
|
this._isPersistentContext = !browserContextId;
|
||||||
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
|
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
|
||||||
|
|
||||||
|
// Create instrumentation per context.
|
||||||
|
this.instrumentation = createInstrumentation();
|
||||||
|
|
||||||
if (this._options.recordHar)
|
if (this._options.recordHar)
|
||||||
this._harRecorder = new HarRecorder(this, { ...this._options.recordHar, path: path.join(this._browser.options.artifactsDir, `${createGuid()}.har`) });
|
this._harRecorder = new HarRecorder(this, { ...this._options.recordHar, path: path.join(this._browser.options.artifactsDir, `${createGuid()}.har`) });
|
||||||
|
|
||||||
|
|
@ -93,9 +96,6 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
async _initialize() {
|
async _initialize() {
|
||||||
if (this.attribution.isInternal)
|
if (this.attribution.isInternal)
|
||||||
return;
|
return;
|
||||||
// Create instrumentation per context.
|
|
||||||
this.instrumentation = createInstrumentation();
|
|
||||||
|
|
||||||
// Debugger will pause execution upon page.pause in headed mode.
|
// Debugger will pause execution upon page.pause in headed mode.
|
||||||
const contextDebugger = new Debugger(this);
|
const contextDebugger = new Debugger(this);
|
||||||
this.instrumentation.addListener(contextDebugger);
|
this.instrumentation.addListener(contextDebugger);
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ export type ScreencastFrameTraceEvent = {
|
||||||
|
|
||||||
export type ActionTraceEvent = {
|
export type ActionTraceEvent = {
|
||||||
type: 'action' | 'event',
|
type: 'action' | 'event',
|
||||||
hasSnapshot: boolean,
|
|
||||||
metadata: CallMetadata,
|
metadata: CallMetadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -260,21 +260,21 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
|
||||||
const pendingCall = this._pendingCalls.get(metadata.id);
|
const pendingCall = this._pendingCalls.get(metadata.id);
|
||||||
if (!pendingCall || pendingCall.afterSnapshot)
|
if (!pendingCall || pendingCall.afterSnapshot)
|
||||||
return;
|
return;
|
||||||
if (!sdkObject.attribution.page) {
|
if (!sdkObject.attribution.context) {
|
||||||
this._pendingCalls.delete(metadata.id);
|
this._pendingCalls.delete(metadata.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pendingCall.afterSnapshot = this._captureSnapshot('after', sdkObject, metadata);
|
pendingCall.afterSnapshot = this._captureSnapshot('after', sdkObject, metadata);
|
||||||
await pendingCall.afterSnapshot;
|
await pendingCall.afterSnapshot;
|
||||||
const event: trace.ActionTraceEvent = { type: 'action', metadata, hasSnapshot: shouldCaptureSnapshot(metadata) };
|
const event: trace.ActionTraceEvent = { type: 'action', metadata };
|
||||||
this._appendTraceEvent(event);
|
this._appendTraceEvent(event);
|
||||||
this._pendingCalls.delete(metadata.id);
|
this._pendingCalls.delete(metadata.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEvent(sdkObject: SdkObject, metadata: CallMetadata) {
|
onEvent(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||||
if (!sdkObject.attribution.page)
|
if (!sdkObject.attribution.context)
|
||||||
return;
|
return;
|
||||||
const event: trace.ActionTraceEvent = { type: 'event', metadata, hasSnapshot: false };
|
const event: trace.ActionTraceEvent = { type: 'event', metadata };
|
||||||
this._appendTraceEvent(event);
|
this._appendTraceEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export type ParsedStackTrace = {
|
||||||
allFrames: StackFrame[];
|
allFrames: StackFrame[];
|
||||||
frames: StackFrame[];
|
frames: StackFrame[];
|
||||||
frameTexts: string[];
|
frameTexts: string[];
|
||||||
apiName: string;
|
apiName: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function captureStackTrace(): ParsedStackTrace {
|
export function captureStackTrace(): ParsedStackTrace {
|
||||||
|
|
|
||||||
58
packages/playwright-core/src/web/traceViewer/entries.ts
Normal file
58
packages/playwright-core/src/web/traceViewer/entries.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ResourceSnapshot } from '../../server/trace/common/snapshotTypes';
|
||||||
|
import * as trace from '../../server/trace/common/traceEvents';
|
||||||
|
|
||||||
|
export type ContextEntry = {
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
browserName: string;
|
||||||
|
options: trace.BrowserContextEventOptions;
|
||||||
|
pages: PageEntry[];
|
||||||
|
resources: ResourceSnapshot[];
|
||||||
|
actions: trace.ActionTraceEvent[];
|
||||||
|
events: trace.ActionTraceEvent[];
|
||||||
|
objects: { [key: string]: any };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PageEntry = {
|
||||||
|
screencastFrames: {
|
||||||
|
sha1: string,
|
||||||
|
timestamp: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
export function createEmptyContext(): ContextEntry {
|
||||||
|
const now = performance.now();
|
||||||
|
return {
|
||||||
|
startTime: now,
|
||||||
|
endTime: now,
|
||||||
|
browserName: '',
|
||||||
|
options: {
|
||||||
|
deviceScaleFactor: 1,
|
||||||
|
isMobile: false,
|
||||||
|
viewport: { width: 1280, height: 800 },
|
||||||
|
_debugName: '<empty>',
|
||||||
|
},
|
||||||
|
pages: [],
|
||||||
|
resources: [],
|
||||||
|
actions: [],
|
||||||
|
events: [],
|
||||||
|
objects: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -15,10 +15,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as trace from '../../server/trace/common/traceEvents';
|
import * as trace from '../../server/trace/common/traceEvents';
|
||||||
import type { ResourceSnapshot } from '../../server/trace/common/snapshotTypes';
|
|
||||||
import { BaseSnapshotStorage } from './snapshotStorage';
|
import { BaseSnapshotStorage } from './snapshotStorage';
|
||||||
|
|
||||||
import type zip from '@zip.js/zip.js';
|
import type zip from '@zip.js/zip.js';
|
||||||
|
import { ContextEntry, createEmptyContext, PageEntry } from './entries';
|
||||||
|
import type { CallMetadata } from '../../protocol/callMetadata';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
self.importScripts('zip.min.js');
|
self.importScripts('zip.min.js');
|
||||||
|
|
||||||
|
|
@ -32,14 +34,7 @@ export class TraceModel {
|
||||||
private _version: number | undefined;
|
private _version: number | undefined;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.contextEntry = {
|
this.contextEntry = createEmptyContext();
|
||||||
startTime: Number.MAX_VALUE,
|
|
||||||
endTime: Number.MIN_VALUE,
|
|
||||||
browserName: '',
|
|
||||||
options: { },
|
|
||||||
pages: [],
|
|
||||||
resources: [],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(traceURL: string) {
|
async load(traceURL: string) {
|
||||||
|
|
@ -85,8 +80,7 @@ export class TraceModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _build() {
|
private _build() {
|
||||||
for (const page of this.contextEntry!.pages)
|
this.contextEntry!.actions.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
|
||||||
page.actions.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
|
|
||||||
this.contextEntry!.resources = this._snapshotStorage!.resources();
|
this.contextEntry!.resources = this._snapshotStorage!.resources();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,9 +88,6 @@ export class TraceModel {
|
||||||
let pageEntry = this.pageEntries.get(pageId);
|
let pageEntry = this.pageEntries.get(pageId);
|
||||||
if (!pageEntry) {
|
if (!pageEntry) {
|
||||||
pageEntry = {
|
pageEntry = {
|
||||||
actions: [],
|
|
||||||
events: [],
|
|
||||||
objects: {},
|
|
||||||
screencastFrames: [],
|
screencastFrames: [],
|
||||||
};
|
};
|
||||||
this.pageEntries.set(pageId, pageEntry);
|
this.pageEntries.set(pageId, pageEntry);
|
||||||
|
|
@ -120,19 +111,18 @@ export class TraceModel {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'action': {
|
case 'action': {
|
||||||
const metadata = event.metadata;
|
const include = !!event.metadata.apiName && !isTracing(event.metadata);
|
||||||
const include = event.hasSnapshot;
|
if (include)
|
||||||
if (include && metadata.pageId)
|
this.contextEntry!.actions.push(event);
|
||||||
this._pageEntry(metadata.pageId).actions.push(event);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'event': {
|
case 'event': {
|
||||||
const metadata = event.metadata;
|
const metadata = event.metadata;
|
||||||
if (metadata.pageId) {
|
if (metadata.pageId) {
|
||||||
if (metadata.method === '__create__')
|
if (metadata.method === '__create__')
|
||||||
this._pageEntry(metadata.pageId).objects[metadata.params.guid] = metadata.params.initializer;
|
this.contextEntry!.objects[metadata.params.guid] = metadata.params.initializer;
|
||||||
else
|
else
|
||||||
this._pageEntry(metadata.pageId).events.push(event);
|
this.contextEntry!.events.push(event);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -161,8 +151,6 @@ export class TraceModel {
|
||||||
if (event.type === 'action') {
|
if (event.type === 'action') {
|
||||||
if (typeof event.metadata.error === 'string')
|
if (typeof event.metadata.error === 'string')
|
||||||
event.metadata.error = { error: { name: 'Error', message: event.metadata.error } };
|
event.metadata.error = { error: { name: 'Error', message: event.metadata.error } };
|
||||||
if (event.metadata && typeof event.hasSnapshot !== 'boolean')
|
|
||||||
event.hasSnapshot = commandsWithTracingSnapshots.has(event.metadata);
|
|
||||||
}
|
}
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
@ -202,27 +190,6 @@ export class TraceModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ContextEntry = {
|
|
||||||
startTime: number;
|
|
||||||
endTime: number;
|
|
||||||
browserName: string;
|
|
||||||
options: trace.BrowserContextEventOptions;
|
|
||||||
pages: PageEntry[];
|
|
||||||
resources: ResourceSnapshot[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PageEntry = {
|
|
||||||
actions: trace.ActionTraceEvent[];
|
|
||||||
events: trace.ActionTraceEvent[];
|
|
||||||
objects: { [key: string]: any };
|
|
||||||
screencastFrames: {
|
|
||||||
sha1: string,
|
|
||||||
timestamp: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export class PersistentSnapshotStorage extends BaseSnapshotStorage {
|
export class PersistentSnapshotStorage extends BaseSnapshotStorage {
|
||||||
private _entries: Map<string, zip.Entry>;
|
private _entries: Map<string, zip.Entry>;
|
||||||
|
|
||||||
|
|
@ -239,96 +206,6 @@ export class PersistentSnapshotStorage extends BaseSnapshotStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prior to version 2 we did not have a hasSnapshot bit on.
|
function isTracing(metadata: CallMetadata): boolean {
|
||||||
export const commandsWithTracingSnapshots = new Set([
|
return metadata.method.startsWith('tracing');
|
||||||
'EventTarget.waitForEventInfo',
|
}
|
||||||
'BrowserContext.waitForEventInfo',
|
|
||||||
'Page.waitForEventInfo',
|
|
||||||
'WebSocket.waitForEventInfo',
|
|
||||||
'ElectronApplication.waitForEventInfo',
|
|
||||||
'AndroidDevice.waitForEventInfo',
|
|
||||||
'Page.goBack',
|
|
||||||
'Page.goForward',
|
|
||||||
'Page.reload',
|
|
||||||
'Page.setViewportSize',
|
|
||||||
'Page.keyboardDown',
|
|
||||||
'Page.keyboardUp',
|
|
||||||
'Page.keyboardInsertText',
|
|
||||||
'Page.keyboardType',
|
|
||||||
'Page.keyboardPress',
|
|
||||||
'Page.mouseMove',
|
|
||||||
'Page.mouseDown',
|
|
||||||
'Page.mouseUp',
|
|
||||||
'Page.mouseClick',
|
|
||||||
'Page.mouseWheel',
|
|
||||||
'Page.touchscreenTap',
|
|
||||||
'Frame.evalOnSelector',
|
|
||||||
'Frame.evalOnSelectorAll',
|
|
||||||
'Frame.addScriptTag',
|
|
||||||
'Frame.addStyleTag',
|
|
||||||
'Frame.check',
|
|
||||||
'Frame.click',
|
|
||||||
'Frame.dragAndDrop',
|
|
||||||
'Frame.dblclick',
|
|
||||||
'Frame.dispatchEvent',
|
|
||||||
'Frame.evaluateExpression',
|
|
||||||
'Frame.evaluateExpressionHandle',
|
|
||||||
'Frame.fill',
|
|
||||||
'Frame.focus',
|
|
||||||
'Frame.getAttribute',
|
|
||||||
'Frame.goto',
|
|
||||||
'Frame.hover',
|
|
||||||
'Frame.innerHTML',
|
|
||||||
'Frame.innerText',
|
|
||||||
'Frame.inputValue',
|
|
||||||
'Frame.isChecked',
|
|
||||||
'Frame.isDisabled',
|
|
||||||
'Frame.isEnabled',
|
|
||||||
'Frame.isHidden',
|
|
||||||
'Frame.isVisible',
|
|
||||||
'Frame.isEditable',
|
|
||||||
'Frame.press',
|
|
||||||
'Frame.selectOption',
|
|
||||||
'Frame.setContent',
|
|
||||||
'Frame.setInputFiles',
|
|
||||||
'Frame.tap',
|
|
||||||
'Frame.textContent',
|
|
||||||
'Frame.type',
|
|
||||||
'Frame.uncheck',
|
|
||||||
'Frame.waitForTimeout',
|
|
||||||
'Frame.waitForFunction',
|
|
||||||
'Frame.waitForSelector',
|
|
||||||
'Frame.expect',
|
|
||||||
'JSHandle.evaluateExpression',
|
|
||||||
'ElementHandle.evaluateExpression',
|
|
||||||
'JSHandle.evaluateExpressionHandle',
|
|
||||||
'ElementHandle.evaluateExpressionHandle',
|
|
||||||
'ElementHandle.evalOnSelector',
|
|
||||||
'ElementHandle.evalOnSelectorAll',
|
|
||||||
'ElementHandle.check',
|
|
||||||
'ElementHandle.click',
|
|
||||||
'ElementHandle.dblclick',
|
|
||||||
'ElementHandle.dispatchEvent',
|
|
||||||
'ElementHandle.fill',
|
|
||||||
'ElementHandle.hover',
|
|
||||||
'ElementHandle.innerHTML',
|
|
||||||
'ElementHandle.innerText',
|
|
||||||
'ElementHandle.inputValue',
|
|
||||||
'ElementHandle.isChecked',
|
|
||||||
'ElementHandle.isDisabled',
|
|
||||||
'ElementHandle.isEditable',
|
|
||||||
'ElementHandle.isEnabled',
|
|
||||||
'ElementHandle.isHidden',
|
|
||||||
'ElementHandle.isVisible',
|
|
||||||
'ElementHandle.press',
|
|
||||||
'ElementHandle.scrollIntoViewIfNeeded',
|
|
||||||
'ElementHandle.selectOption',
|
|
||||||
'ElementHandle.selectText',
|
|
||||||
'ElementHandle.setInputFiles',
|
|
||||||
'ElementHandle.tap',
|
|
||||||
'ElementHandle.textContent',
|
|
||||||
'ElementHandle.type',
|
|
||||||
'ElementHandle.uncheck',
|
|
||||||
'ElementHandle.waitForElementState',
|
|
||||||
'ElementHandle.waitForSelector'
|
|
||||||
]);
|
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,13 @@ export const ConsoleTab: React.FunctionComponent<{
|
||||||
if (!action)
|
if (!action)
|
||||||
return [];
|
return [];
|
||||||
const entries: { message?: channels.ConsoleMessageInitializer, error?: channels.SerializedError }[] = [];
|
const entries: { message?: channels.ConsoleMessageInitializer, error?: channels.SerializedError }[] = [];
|
||||||
const page = modelUtil.page(action);
|
const context = modelUtil.context(action);
|
||||||
for (const event of modelUtil.eventsForAction(action)) {
|
for (const event of modelUtil.eventsForAction(action)) {
|
||||||
if (event.metadata.method !== 'console' && event.metadata.method !== 'pageError')
|
if (event.metadata.method !== 'console' && event.metadata.method !== 'pageError')
|
||||||
continue;
|
continue;
|
||||||
if (event.metadata.method === 'console') {
|
if (event.metadata.method === 'console') {
|
||||||
const { guid } = event.metadata.params.message;
|
const { guid } = event.metadata.params.message;
|
||||||
entries.push({ message: page.objects[guid] });
|
entries.push({ message: context.objects[guid] });
|
||||||
}
|
}
|
||||||
if (event.metadata.method === 'pageError')
|
if (event.metadata.method === 'pageError')
|
||||||
entries.push({ error: event.metadata.params.error });
|
entries.push({ error: event.metadata.params.error });
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { Boundaries, Size } from '../geometry';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useMeasure } from './helpers';
|
import { useMeasure } from './helpers';
|
||||||
import { upperBound } from '../../uiUtils';
|
import { upperBound } from '../../uiUtils';
|
||||||
import { ContextEntry, PageEntry } from '../traceModel';
|
import { ContextEntry, PageEntry } from '../entries';
|
||||||
|
|
||||||
const tileSize = { width: 200, height: 45 };
|
const tileSize = { width: 200, height: 45 };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,38 +16,29 @@
|
||||||
|
|
||||||
import { ResourceSnapshot } from '../../../server/trace/common/snapshotTypes';
|
import { ResourceSnapshot } from '../../../server/trace/common/snapshotTypes';
|
||||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||||
import { ContextEntry, PageEntry } from '../traceModel';
|
import { ContextEntry } from '../entries';
|
||||||
|
|
||||||
const contextSymbol = Symbol('context');
|
const contextSymbol = Symbol('context');
|
||||||
const pageSymbol = Symbol('context');
|
|
||||||
const nextSymbol = Symbol('next');
|
const nextSymbol = Symbol('next');
|
||||||
const eventsSymbol = Symbol('events');
|
const eventsSymbol = Symbol('events');
|
||||||
const resourcesSymbol = Symbol('resources');
|
const resourcesSymbol = Symbol('resources');
|
||||||
|
|
||||||
export function indexModel(context: ContextEntry) {
|
export function indexModel(context: ContextEntry) {
|
||||||
for (const page of context.pages) {
|
for (const page of context.pages)
|
||||||
(page as any)[contextSymbol] = context;
|
(page as any)[contextSymbol] = context;
|
||||||
for (let i = 0; i < page.actions.length; ++i) {
|
for (let i = 0; i < context.actions.length; ++i) {
|
||||||
const action = page.actions[i] as any;
|
const action = context.actions[i] as any;
|
||||||
action[contextSymbol] = context;
|
action[contextSymbol] = context;
|
||||||
action[pageSymbol] = page;
|
action[nextSymbol] = context.actions[i + 1];
|
||||||
action[nextSymbol] = page.actions[i + 1];
|
|
||||||
}
|
}
|
||||||
for (const event of page.events) {
|
for (const event of context.events)
|
||||||
(event as any)[contextSymbol] = context;
|
(event as any)[contextSymbol] = context;
|
||||||
(event as any)[pageSymbol] = page;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function context(action: ActionTraceEvent): ContextEntry {
|
export function context(action: ActionTraceEvent): ContextEntry {
|
||||||
return (action as any)[contextSymbol];
|
return (action as any)[contextSymbol];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function page(action: ActionTraceEvent): PageEntry {
|
|
||||||
return (action as any)[pageSymbol];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function next(action: ActionTraceEvent): ActionTraceEvent {
|
export function next(action: ActionTraceEvent): ActionTraceEvent {
|
||||||
return (action as any)[nextSymbol];
|
return (action as any)[nextSymbol];
|
||||||
}
|
}
|
||||||
|
|
@ -55,11 +46,11 @@ export function next(action: ActionTraceEvent): ActionTraceEvent {
|
||||||
export function stats(action: ActionTraceEvent): { errors: number, warnings: number } {
|
export function stats(action: ActionTraceEvent): { errors: number, warnings: number } {
|
||||||
let errors = 0;
|
let errors = 0;
|
||||||
let warnings = 0;
|
let warnings = 0;
|
||||||
const p = page(action);
|
const c = context(action);
|
||||||
for (const event of eventsForAction(action)) {
|
for (const event of eventsForAction(action)) {
|
||||||
if (event.metadata.method === 'console') {
|
if (event.metadata.method === 'console') {
|
||||||
const { guid } = event.metadata.params.message;
|
const { guid } = event.metadata.params.message;
|
||||||
const type = p.objects[guid]?.type;
|
const type = c.objects[guid]?.type;
|
||||||
if (type === 'warning')
|
if (type === 'warning')
|
||||||
++warnings;
|
++warnings;
|
||||||
else if (type === 'error')
|
else if (type === 'error')
|
||||||
|
|
@ -77,7 +68,7 @@ export function eventsForAction(action: ActionTraceEvent): ActionTraceEvent[] {
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
const nextAction = next(action);
|
const nextAction = next(action);
|
||||||
result = page(action).events.filter(event => {
|
result = context(action).events.filter(event => {
|
||||||
return event.metadata.startTime >= action.metadata.startTime && (!nextAction || event.metadata.startTime < nextAction.metadata.startTime);
|
return event.metadata.startTime >= action.metadata.startTime && (!nextAction || event.metadata.startTime < nextAction.metadata.startTime);
|
||||||
});
|
});
|
||||||
(action as any)[eventsSymbol] = result;
|
(action as any)[eventsSymbol] = result;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||||
import { ContextEntry } from '../traceModel';
|
import { ContextEntry } from '../entries';
|
||||||
import './timeline.css';
|
import './timeline.css';
|
||||||
import { Boundaries } from '../geometry';
|
import { Boundaries } from '../geometry';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
@ -56,8 +56,7 @@ export const Timeline: React.FunctionComponent<{
|
||||||
|
|
||||||
const bars = React.useMemo(() => {
|
const bars = React.useMemo(() => {
|
||||||
const bars: TimelineBar[] = [];
|
const bars: TimelineBar[] = [];
|
||||||
for (const page of context.pages) {
|
for (const entry of context.actions) {
|
||||||
for (const entry of page.actions) {
|
|
||||||
let detail = trimRight(entry.metadata.params.selector || '', 50);
|
let detail = trimRight(entry.metadata.params.selector || '', 50);
|
||||||
if (entry.metadata.method === 'goto')
|
if (entry.metadata.method === 'goto')
|
||||||
detail = trimRight(entry.metadata.params.url || '', 50);
|
detail = trimRight(entry.metadata.params.url || '', 50);
|
||||||
|
|
@ -73,7 +72,7 @@ export const Timeline: React.FunctionComponent<{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const event of page.events) {
|
for (const event of context.events) {
|
||||||
const startTime = event.metadata.startTime;
|
const startTime = event.metadata.startTime;
|
||||||
bars.push({
|
bars.push({
|
||||||
event,
|
event,
|
||||||
|
|
@ -86,7 +85,6 @@ export const Timeline: React.FunctionComponent<{
|
||||||
className: `${event.metadata.type}_${event.metadata.method}`.toLowerCase()
|
className: `${event.metadata.type}_${event.metadata.method}`.toLowerCase()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return bars;
|
return bars;
|
||||||
}, [context, boundaries, measure.width]);
|
}, [context, boundaries, measure.width]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||||
import { ContextEntry } from '../traceModel';
|
import { ContextEntry, createEmptyContext } from '../entries';
|
||||||
import { ActionList } from './actionList';
|
import { ActionList } from './actionList';
|
||||||
import { TabbedPane } from './tabbedPane';
|
import { TabbedPane } from './tabbedPane';
|
||||||
import { Timeline } from './timeline';
|
import { Timeline } from './timeline';
|
||||||
|
|
@ -49,13 +49,6 @@ export const Workbench: React.FunctionComponent<{
|
||||||
})();
|
})();
|
||||||
}, [traceURL]);
|
}, [traceURL]);
|
||||||
|
|
||||||
const actions = React.useMemo(() => {
|
|
||||||
const actions: ActionTraceEvent[] = [];
|
|
||||||
for (const page of contextEntry.pages)
|
|
||||||
actions.push(...page.actions);
|
|
||||||
return actions;
|
|
||||||
}, [contextEntry]);
|
|
||||||
|
|
||||||
const defaultSnapshotSize = contextEntry.options.viewport || { width: 1280, height: 720 };
|
const defaultSnapshotSize = contextEntry.options.viewport || { width: 1280, height: 720 };
|
||||||
const boundaries = { minimum: contextEntry.startTime, maximum: contextEntry.endTime };
|
const boundaries = { minimum: contextEntry.startTime, maximum: contextEntry.endTime };
|
||||||
|
|
||||||
|
|
@ -98,7 +91,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
]} selectedTab={selectedTab} setSelectedTab={setSelectedTab}/>
|
]} selectedTab={selectedTab} setSelectedTab={setSelectedTab}/>
|
||||||
</SplitView>
|
</SplitView>
|
||||||
<ActionList
|
<ActionList
|
||||||
actions={actions}
|
actions={contextEntry.actions}
|
||||||
selectedAction={selectedAction}
|
selectedAction={selectedAction}
|
||||||
highlightedAction={highlightedAction}
|
highlightedAction={highlightedAction}
|
||||||
onSelected={action => {
|
onSelected={action => {
|
||||||
|
|
@ -111,17 +104,4 @@ export const Workbench: React.FunctionComponent<{
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const now = performance.now();
|
const emptyContext = createEmptyContext();
|
||||||
const emptyContext: ContextEntry = {
|
|
||||||
startTime: now,
|
|
||||||
endTime: now,
|
|
||||||
browserName: '',
|
|
||||||
options: {
|
|
||||||
deviceScaleFactor: 1,
|
|
||||||
isMobile: false,
|
|
||||||
viewport: { width: 1280, height: 800 },
|
|
||||||
_debugName: '<empty>',
|
|
||||||
},
|
|
||||||
pages: [],
|
|
||||||
resources: [],
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ class HtmlReporter {
|
||||||
const stats = builder.build(reports);
|
const stats = builder.build(reports);
|
||||||
|
|
||||||
if (!stats.ok && !process.env.CI && !process.env.PWTEST_SKIP_TEST_OUTPUT) {
|
if (!stats.ok && !process.env.CI && !process.env.PWTEST_SKIP_TEST_OUTPUT) {
|
||||||
showHTMLReport(reportFolder);
|
await showHTMLReport(reportFolder);
|
||||||
} else {
|
} else {
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
@ -168,7 +168,6 @@ export async function showHTMLReport(reportFolder: string | undefined) {
|
||||||
const url = await server.start(9323);
|
const url = await server.start(9323);
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`));
|
console.log(colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`));
|
||||||
console.log('');
|
|
||||||
open(url);
|
open(url);
|
||||||
process.on('SIGINT', () => process.exit(0));
|
process.on('SIGINT', () => process.exit(0));
|
||||||
await new Promise(() => {});
|
await new Promise(() => {});
|
||||||
|
|
|
||||||
|
|
@ -904,7 +904,7 @@ it('context request should export same storage state as context', async ({ conte
|
||||||
expect(pageState).toEqual(contextState);
|
expect(pageState).toEqual(contextState);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept bool and numeric params', async ({ context, page, server }) => {
|
it('should accept bool and numeric params', async ({ page, server }) => {
|
||||||
let request;
|
let request;
|
||||||
const url = new URL(server.EMPTY_PAGE);
|
const url = new URL(server.EMPTY_PAGE);
|
||||||
url.searchParams.set('str', 's');
|
url.searchParams.set('str', 's');
|
||||||
|
|
|
||||||
|
|
@ -180,17 +180,19 @@ test('should show empty trace viewer', async ({ showTraceViewer }, testInfo) =>
|
||||||
test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer(traceFile);
|
const traceViewer = await showTraceViewer(traceFile);
|
||||||
await expect(traceViewer.actionTitles).toHaveText([
|
await expect(traceViewer.actionTitles).toHaveText([
|
||||||
|
/browserContext.newPage— [\d.ms]+/,
|
||||||
/page.gotodata:text\/html,<html>Hello world<\/html>— [\d.ms]+/,
|
/page.gotodata:text\/html,<html>Hello world<\/html>— [\d.ms]+/,
|
||||||
/page.setContent— [\d.ms]+/,
|
/page.setContent— [\d.ms]+/,
|
||||||
/expect.toHaveTextbutton— [\d.ms]+/,
|
/expect.toHaveTextbutton— [\d.ms]+/,
|
||||||
/page.evaluate— [\d.ms]+/,
|
/page.evaluate— [\d.ms]+/,
|
||||||
/page.click"Click"— [\d.ms]+/,
|
/page.click"Click"— [\d.ms]+/,
|
||||||
/page.waitForEvent— [\d.ms]+/,
|
/page.waitForEvent— [\d.ms]+/,
|
||||||
|
/page.route— [\d.ms]+/,
|
||||||
/page.waitForNavigation— [\d.ms]+/,
|
/page.waitForNavigation— [\d.ms]+/,
|
||||||
/page.waitForTimeout— [\d.ms]+/,
|
/page.waitForTimeout— [\d.ms]+/,
|
||||||
/page.gotohttp:\/\/localhost:\d+\/frames\/frame.html— [\d.ms]+/,
|
/page.gotohttp:\/\/localhost:\d+\/frames\/frame.html— [\d.ms]+/,
|
||||||
|
/route.continue— [\d.ms]+/,
|
||||||
/page.setViewportSize— [\d.ms]+/,
|
/page.setViewportSize— [\d.ms]+/,
|
||||||
/page.hoverbody— [\d.ms]+/,
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -261,12 +263,6 @@ test('should have correct stack trace', async ({ showTraceViewer }) => {
|
||||||
/doClick\s+trace-viewer.spec.ts\s+:\d+/,
|
/doClick\s+trace-viewer.spec.ts\s+:\d+/,
|
||||||
/recordTrace\s+trace-viewer.spec.ts\s+:\d+/,
|
/recordTrace\s+trace-viewer.spec.ts\s+:\d+/,
|
||||||
], { useInnerText: true });
|
], { useInnerText: true });
|
||||||
|
|
||||||
await traceViewer.selectAction('page.hover');
|
|
||||||
await traceViewer.showSourceTab();
|
|
||||||
await expect(traceViewer.stackFrames).toContainText([
|
|
||||||
/BrowserType.browserType._onWillCloseContext\s+trace-viewer.spec.ts\s+:\d+/,
|
|
||||||
], { useInnerText: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should have network requests', async ({ showTraceViewer }) => {
|
test('should have network requests', async ({ showTraceViewer }) => {
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ DEPS['src/cli/driver.ts'] = DEPS['src/inProcessFactory.ts'] = DEPS['src/browserS
|
||||||
|
|
||||||
// Tracing is a client/server plugin, nothing should depend on it.
|
// Tracing is a client/server plugin, nothing should depend on it.
|
||||||
DEPS['src/web/recorder/'] = ['src/common/', 'src/web/', 'src/web/components/', 'src/server/supplements/recorder/recorderTypes.ts'];
|
DEPS['src/web/recorder/'] = ['src/common/', 'src/web/', 'src/web/components/', 'src/server/supplements/recorder/recorderTypes.ts'];
|
||||||
DEPS['src/web/traceViewer/'] = ['src/common/', 'src/web/', 'src/server/trace/common/'];
|
DEPS['src/web/traceViewer/'] = ['src/common/', 'src/web/', 'src/server/trace/common/', 'src/protocol/callMetadata.ts'];
|
||||||
DEPS['src/web/traceViewer/ui/'] = ['src/common/', 'src/protocol/', 'src/web/traceViewer/', 'src/web/', 'src/server/trace/viewer/', 'src/server/trace/', 'src/server/trace/common/', 'src/server/snapshot/snapshotTypes.ts', 'src/protocol/channels.ts'];
|
DEPS['src/web/traceViewer/ui/'] = ['src/common/', 'src/protocol/', 'src/web/traceViewer/', 'src/web/', 'src/server/trace/viewer/', 'src/server/trace/', 'src/server/trace/common/', 'src/server/snapshot/snapshotTypes.ts', 'src/protocol/channels.ts'];
|
||||||
DEPS['src/web/traceViewer/inMemorySnapshotter.ts'] = ['src/**'];
|
DEPS['src/web/traceViewer/inMemorySnapshotter.ts'] = ['src/**'];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue