feat(tracing): make context.request appear in the trace (#9555)

This commit is contained in:
Pavel Feldman 2021-10-15 14:22:49 -08:00 committed by GitHub
parent 4ce765c3ae
commit e31b96cc26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 177 additions and 268 deletions

View file

@ -238,7 +238,7 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel, c
return this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => {
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
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}"`);
if (event !== Events.AndroidDevice.Close)
waiter.rejectOnEvent(this, Events.AndroidDevice.Close, new Error('Device closed'));

View file

@ -151,7 +151,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
}
if (!handled) {
// it can race with BrowserContext.close() which then throws since its closed
route.continue().catch(() => {});
route._internalContinue();
} else {
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) => {
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
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}"`);
if (event !== Events.BrowserContext.Close)
waiter.rejectOnEvent(this, Events.BrowserContext.Close, new Error('Context closed'));

View file

@ -82,7 +82,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
if (validator) {
return (params: any) => {
if (callCookie && csi) {
callCookie.userObject = csi.onApiCallBegin(renderCallWithParams(stackTrace!.apiName, params)).userObject;
callCookie.userObject = csi.onApiCallBegin(renderCallWithParams(stackTrace!.apiName!, params)).userObject;
csi = undefined;
}
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;
}
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;
const stackTrace = captureStackTrace();
const { apiName, frameTexts } = stackTrace;
@ -106,23 +106,25 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
ancestorWithCSI = ancestorWithCSI._parent;
// Do not report nested async calls to _wrapApiCall.
const isNested = stackTrace.allFrames.filter(f => f.function?.includes('_wrapApiCall')).length > 1;
const csi = isNested ? undefined : ancestorWithCSI._csi;
isInternal = isInternal || stackTrace.allFrames.filter(f => f.function?.includes('_wrapApiCall')).length > 1;
if (isInternal)
delete stackTrace.apiName;
const csi = isInternal ? undefined : ancestorWithCSI._csi;
const callCookie: { userObject: any } = { userObject: null };
try {
logApiCall(logger, `=> ${apiName} started`, isNested);
logApiCall(logger, `=> ${apiName} started`, isInternal);
const channel = this._createChannel({}, stackTrace, csi, callCookie);
const result = await func(channel as any, stackTrace);
csi?.onApiCallEnd(callCookie);
logApiCall(logger, `<= ${apiName} succeeded`, isNested);
logApiCall(logger, `<= ${apiName} succeeded`, isInternal);
return result;
} catch (e) {
const innerError = ((process.env.PWDEBUGIMPL || isUnderTest()) && e.stack) ? '\n<inner error>\n' + e.stack : '';
e.message = apiName + ': ' + e.message;
e.stack = e.message + '\n' + frameTexts.join('\n') + innerError;
csi?.onApiCallEnd(callCookie, e);
logApiCall(logger, `<= ${apiName} failed`, isNested);
logApiCall(logger, `<= ${apiName} failed`, isInternal);
throw e;
}
}

View file

@ -108,7 +108,7 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
return this._wrapApiCall(async (channel: channels.ElectronApplicationChannel) => {
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
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}"`);
if (event !== Events.ElectronApplication.Close)
waiter.rejectOnEvent(this, Events.ElectronApplication.Close, new Error('Electron application closed'));

View file

@ -94,8 +94,8 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
});
}
private _setupNavigationWaiter(options: { timeout?: number }): Waiter {
const waiter = new Waiter(this._page!, '');
private _setupNavigationWaiter(channel: channels.EventTargetChannel, options: { timeout?: number }): Waiter {
const waiter = new Waiter(channel, '');
if (this._page!.isClosed())
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!'));
@ -107,9 +107,9 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
}
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 waiter = this._setupNavigationWaiter(options);
const waiter = this._setupNavigationWaiter(channel, options);
const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : '';
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 response = request ? await waiter.waitForPromise(request._finalRequest().response()) : null;
const response = request ? await waiter.waitForPromise(request._finalRequest()._internalResponse()) : null;
waiter.dispose();
return response;
});
@ -145,8 +145,8 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
state = verifyLoadState('state', state);
if (this._loadStates.has(state))
return;
return this._wrapApiCall(async (channel: channels.FrameChannel) => {
const waiter = this._setupNavigationWaiter(options);
return this._page!._wrapApiCall(async (channel: channels.PageChannel) => {
const waiter = this._setupNavigationWaiter(channel, options);
await waiter.waitForEvent<LifecycleEvent>(this._eventEmitter, 'loadstate', s => {
waiter.log(` "${s}" event fired`);
return s === state;

View file

@ -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 {
return Frame.from(this._initializer.frame);
}
@ -386,9 +392,13 @@ export class Route extends ChannelOwner<channels.RouteChannel, channels.RouteIni
await this._continue(options, false);
}
async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: NotInterceptResponse): Promise<null>;
async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: InterceptResponse): Promise<api.Response>;
async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: boolean): Promise<null|api.Response> {
async _internalContinue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer } = {}) {
await this._continue(options, false, true).catch(() => {});
}
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) => {
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
const result = await channel.continue({
@ -401,7 +411,7 @@ export class Route extends ChannelOwner<channels.RouteChannel, channels.RouteIni
if (result.response)
return new InterceptedResponse(this, result.response);
return null;
});
}, undefined, isInternal);
}
async _responseBody(): Promise<Buffer> {
@ -585,7 +595,7 @@ export class WebSocket extends ChannelOwner<channels.WebSocketChannel, channels.
return this._wrapApiCall(async (channel: channels.WebSocketChannel) => {
const timeout = this._page._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
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}"`);
if (event !== Events.WebSocket.Error)
waiter.rejectOnEvent(this, Events.WebSocket.Error, new Error('Socket error'));

View file

@ -369,7 +369,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
};
const trimmedUrl = trimUrl(urlOrPredicate);
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 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> {
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 predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
const waiter = Waiter.createForEvent(channel, event);
if (logLine)
waiter.log(logLine);
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);

View file

@ -19,7 +19,6 @@ import { rewriteErrorMessage } from '../utils/stackTrace';
import { TimeoutError } from '../utils/errors';
import { createGuid } from '../utils/utils';
import * as channels from '../protocol/channels';
import { ChannelOwner } from './channelOwner';
export class Waiter {
private _dispose: (() => void)[];
@ -31,19 +30,17 @@ export class Waiter {
private _waitId: string;
private _error: string | undefined;
constructor(channelOwner: ChannelOwner<channels.EventTargetChannel>, event: string) {
constructor(channel: channels.EventTargetChannel, event: string) {
this._waitId = createGuid();
this._channel = channelOwner._channel;
channelOwner._wrapApiCall(async (channel: channels.EventTargetChannel) => {
channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'before', event } }).catch(() => {});
});
this._channel = channel;
this._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'before', event } }).catch(() => {});
this._dispose = [
() => this._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'after', error: this._error } }).catch(() => {})
];
}
static createForEvent(channelOwner: ChannelOwner<channels.EventTargetChannel>, event: string) {
return new Waiter(channelOwner, event);
static createForEvent(channel: channels.EventTargetChannel, event: string) {
return new Waiter(channel, event);
}
async waitForEvent<T = void>(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean | Promise<boolean>): Promise<T> {

View file

@ -75,6 +75,9 @@ export abstract class BrowserContext extends SdkObject {
this._isPersistentContext = !browserContextId;
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
// Create instrumentation per context.
this.instrumentation = createInstrumentation();
if (this._options.recordHar)
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() {
if (this.attribution.isInternal)
return;
// Create instrumentation per context.
this.instrumentation = createInstrumentation();
// Debugger will pause execution upon page.pause in headed mode.
const contextDebugger = new Debugger(this);
this.instrumentation.addListener(contextDebugger);

View file

@ -45,7 +45,6 @@ export type ScreencastFrameTraceEvent = {
export type ActionTraceEvent = {
type: 'action' | 'event',
hasSnapshot: boolean,
metadata: CallMetadata,
};

View file

@ -260,21 +260,21 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
const pendingCall = this._pendingCalls.get(metadata.id);
if (!pendingCall || pendingCall.afterSnapshot)
return;
if (!sdkObject.attribution.page) {
if (!sdkObject.attribution.context) {
this._pendingCalls.delete(metadata.id);
return;
}
pendingCall.afterSnapshot = this._captureSnapshot('after', sdkObject, metadata);
await pendingCall.afterSnapshot;
const event: trace.ActionTraceEvent = { type: 'action', metadata, hasSnapshot: shouldCaptureSnapshot(metadata) };
const event: trace.ActionTraceEvent = { type: 'action', metadata };
this._appendTraceEvent(event);
this._pendingCalls.delete(metadata.id);
}
onEvent(sdkObject: SdkObject, metadata: CallMetadata) {
if (!sdkObject.attribution.page)
if (!sdkObject.attribution.context)
return;
const event: trace.ActionTraceEvent = { type: 'event', metadata, hasSnapshot: false };
const event: trace.ActionTraceEvent = { type: 'event', metadata };
this._appendTraceEvent(event);
}

View file

@ -38,7 +38,7 @@ export type ParsedStackTrace = {
allFrames: StackFrame[];
frames: StackFrame[];
frameTexts: string[];
apiName: string;
apiName: string | undefined;
};
export function captureStackTrace(): ParsedStackTrace {

View 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: {},
};
}

View file

@ -15,10 +15,12 @@
*/
import * as trace from '../../server/trace/common/traceEvents';
import type { ResourceSnapshot } from '../../server/trace/common/snapshotTypes';
import { BaseSnapshotStorage } from './snapshotStorage';
import type zip from '@zip.js/zip.js';
import { ContextEntry, createEmptyContext, PageEntry } from './entries';
import type { CallMetadata } from '../../protocol/callMetadata';
// @ts-ignore
self.importScripts('zip.min.js');
@ -32,14 +34,7 @@ export class TraceModel {
private _version: number | undefined;
constructor() {
this.contextEntry = {
startTime: Number.MAX_VALUE,
endTime: Number.MIN_VALUE,
browserName: '',
options: { },
pages: [],
resources: [],
};
this.contextEntry = createEmptyContext();
}
async load(traceURL: string) {
@ -85,8 +80,7 @@ export class TraceModel {
}
private _build() {
for (const page of this.contextEntry!.pages)
page.actions.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
this.contextEntry!.actions.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
this.contextEntry!.resources = this._snapshotStorage!.resources();
}
@ -94,9 +88,6 @@ export class TraceModel {
let pageEntry = this.pageEntries.get(pageId);
if (!pageEntry) {
pageEntry = {
actions: [],
events: [],
objects: {},
screencastFrames: [],
};
this.pageEntries.set(pageId, pageEntry);
@ -120,19 +111,18 @@ export class TraceModel {
break;
}
case 'action': {
const metadata = event.metadata;
const include = event.hasSnapshot;
if (include && metadata.pageId)
this._pageEntry(metadata.pageId).actions.push(event);
const include = !!event.metadata.apiName && !isTracing(event.metadata);
if (include)
this.contextEntry!.actions.push(event);
break;
}
case 'event': {
const metadata = event.metadata;
if (metadata.pageId) {
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
this._pageEntry(metadata.pageId).events.push(event);
this.contextEntry!.events.push(event);
}
break;
}
@ -161,8 +151,6 @@ export class TraceModel {
if (event.type === 'action') {
if (typeof event.metadata.error === 'string')
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;
}
@ -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 {
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.
export const commandsWithTracingSnapshots = new Set([
'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'
]);
function isTracing(metadata: CallMetadata): boolean {
return metadata.method.startsWith('tracing');
}

View file

@ -27,13 +27,13 @@ export const ConsoleTab: React.FunctionComponent<{
if (!action)
return [];
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)) {
if (event.metadata.method !== 'console' && event.metadata.method !== 'pageError')
continue;
if (event.metadata.method === 'console') {
const { guid } = event.metadata.params.message;
entries.push({ message: page.objects[guid] });
entries.push({ message: context.objects[guid] });
}
if (event.metadata.method === 'pageError')
entries.push({ error: event.metadata.params.error });

View file

@ -19,7 +19,7 @@ import { Boundaries, Size } from '../geometry';
import * as React from 'react';
import { useMeasure } from './helpers';
import { upperBound } from '../../uiUtils';
import { ContextEntry, PageEntry } from '../traceModel';
import { ContextEntry, PageEntry } from '../entries';
const tileSize = { width: 200, height: 45 };

View file

@ -16,38 +16,29 @@
import { ResourceSnapshot } from '../../../server/trace/common/snapshotTypes';
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
import { ContextEntry, PageEntry } from '../traceModel';
import { ContextEntry } from '../entries';
const contextSymbol = Symbol('context');
const pageSymbol = Symbol('context');
const nextSymbol = Symbol('next');
const eventsSymbol = Symbol('events');
const resourcesSymbol = Symbol('resources');
export function indexModel(context: ContextEntry) {
for (const page of context.pages) {
for (const page of context.pages)
(page as any)[contextSymbol] = context;
for (let i = 0; i < page.actions.length; ++i) {
const action = page.actions[i] as any;
action[contextSymbol] = context;
action[pageSymbol] = page;
action[nextSymbol] = page.actions[i + 1];
}
for (const event of page.events) {
(event as any)[contextSymbol] = context;
(event as any)[pageSymbol] = page;
}
for (let i = 0; i < context.actions.length; ++i) {
const action = context.actions[i] as any;
action[contextSymbol] = context;
action[nextSymbol] = context.actions[i + 1];
}
for (const event of context.events)
(event as any)[contextSymbol] = context;
}
export function context(action: ActionTraceEvent): ContextEntry {
return (action as any)[contextSymbol];
}
export function page(action: ActionTraceEvent): PageEntry {
return (action as any)[pageSymbol];
}
export function next(action: ActionTraceEvent): ActionTraceEvent {
return (action as any)[nextSymbol];
}
@ -55,11 +46,11 @@ export function next(action: ActionTraceEvent): ActionTraceEvent {
export function stats(action: ActionTraceEvent): { errors: number, warnings: number } {
let errors = 0;
let warnings = 0;
const p = page(action);
const c = context(action);
for (const event of eventsForAction(action)) {
if (event.metadata.method === 'console') {
const { guid } = event.metadata.params.message;
const type = p.objects[guid]?.type;
const type = c.objects[guid]?.type;
if (type === 'warning')
++warnings;
else if (type === 'error')
@ -77,7 +68,7 @@ export function eventsForAction(action: ActionTraceEvent): ActionTraceEvent[] {
return result;
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);
});
(action as any)[eventsSymbol] = result;

View file

@ -16,7 +16,7 @@
*/
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
import { ContextEntry } from '../traceModel';
import { ContextEntry } from '../entries';
import './timeline.css';
import { Boundaries } from '../geometry';
import * as React from 'react';
@ -56,36 +56,34 @@ export const Timeline: React.FunctionComponent<{
const bars = React.useMemo(() => {
const bars: TimelineBar[] = [];
for (const page of context.pages) {
for (const entry of page.actions) {
let detail = trimRight(entry.metadata.params.selector || '', 50);
if (entry.metadata.method === 'goto')
detail = trimRight(entry.metadata.params.url || '', 50);
bars.push({
action: entry,
leftTime: entry.metadata.startTime,
rightTime: entry.metadata.endTime,
leftPosition: timeToPosition(measure.width, boundaries, entry.metadata.startTime),
rightPosition: timeToPosition(measure.width, boundaries, entry.metadata.endTime),
label: entry.metadata.apiName + ' ' + detail,
type: entry.metadata.type + '.' + entry.metadata.method,
className: `${entry.metadata.type}_${entry.metadata.method}`.toLowerCase()
});
}
for (const entry of context.actions) {
let detail = trimRight(entry.metadata.params.selector || '', 50);
if (entry.metadata.method === 'goto')
detail = trimRight(entry.metadata.params.url || '', 50);
bars.push({
action: entry,
leftTime: entry.metadata.startTime,
rightTime: entry.metadata.endTime,
leftPosition: timeToPosition(measure.width, boundaries, entry.metadata.startTime),
rightPosition: timeToPosition(measure.width, boundaries, entry.metadata.endTime),
label: entry.metadata.apiName + ' ' + detail,
type: entry.metadata.type + '.' + entry.metadata.method,
className: `${entry.metadata.type}_${entry.metadata.method}`.toLowerCase()
});
}
for (const event of page.events) {
const startTime = event.metadata.startTime;
bars.push({
event,
leftTime: startTime,
rightTime: startTime,
leftPosition: timeToPosition(measure.width, boundaries, startTime),
rightPosition: timeToPosition(measure.width, boundaries, startTime),
label: event.metadata.method,
type: event.metadata.type + '.' + event.metadata.method,
className: `${event.metadata.type}_${event.metadata.method}`.toLowerCase()
});
}
for (const event of context.events) {
const startTime = event.metadata.startTime;
bars.push({
event,
leftTime: startTime,
rightTime: startTime,
leftPosition: timeToPosition(measure.width, boundaries, startTime),
rightPosition: timeToPosition(measure.width, boundaries, startTime),
label: event.metadata.method,
type: event.metadata.type + '.' + event.metadata.method,
className: `${event.metadata.type}_${event.metadata.method}`.toLowerCase()
});
}
return bars;
}, [context, boundaries, measure.width]);

View file

@ -15,7 +15,7 @@
*/
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
import { ContextEntry } from '../traceModel';
import { ContextEntry, createEmptyContext } from '../entries';
import { ActionList } from './actionList';
import { TabbedPane } from './tabbedPane';
import { Timeline } from './timeline';
@ -49,13 +49,6 @@ export const Workbench: React.FunctionComponent<{
})();
}, [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 boundaries = { minimum: contextEntry.startTime, maximum: contextEntry.endTime };
@ -98,7 +91,7 @@ export const Workbench: React.FunctionComponent<{
]} selectedTab={selectedTab} setSelectedTab={setSelectedTab}/>
</SplitView>
<ActionList
actions={actions}
actions={contextEntry.actions}
selectedAction={selectedAction}
highlightedAction={highlightedAction}
onSelected={action => {
@ -111,17 +104,4 @@ export const Workbench: React.FunctionComponent<{
</div>;
};
const now = performance.now();
const emptyContext: ContextEntry = {
startTime: now,
endTime: now,
browserName: '',
options: {
deviceScaleFactor: 1,
isMobile: false,
viewport: { width: 1280, height: 800 },
_debugName: '<empty>',
},
pages: [],
resources: [],
};
const emptyContext = createEmptyContext();

View file

@ -127,7 +127,7 @@ class HtmlReporter {
const stats = builder.build(reports);
if (!stats.ok && !process.env.CI && !process.env.PWTEST_SKIP_TEST_OUTPUT) {
showHTMLReport(reportFolder);
await showHTMLReport(reportFolder);
} else {
console.log('');
console.log('');
@ -168,7 +168,6 @@ export async function showHTMLReport(reportFolder: string | undefined) {
const url = await server.start(9323);
console.log('');
console.log(colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`));
console.log('');
open(url);
process.on('SIGINT', () => process.exit(0));
await new Promise(() => {});

View file

@ -904,7 +904,7 @@ it('context request should export same storage state as context', async ({ conte
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;
const url = new URL(server.EMPTY_PAGE);
url.searchParams.set('str', 's');

View file

@ -180,17 +180,19 @@ test('should show empty trace viewer', async ({ showTraceViewer }, testInfo) =>
test('should open simple trace viewer', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer(traceFile);
await expect(traceViewer.actionTitles).toHaveText([
/browserContext.newPage— [\d.ms]+/,
/page.gotodata:text\/html,<html>Hello world<\/html>— [\d.ms]+/,
/page.setContent— [\d.ms]+/,
/expect.toHaveTextbutton— [\d.ms]+/,
/page.evaluate— [\d.ms]+/,
/page.click"Click"— [\d.ms]+/,
/page.waitForEvent— [\d.ms]+/,
/page.route— [\d.ms]+/,
/page.waitForNavigation— [\d.ms]+/,
/page.waitForTimeout— [\d.ms]+/,
/page.gotohttp:\/\/localhost:\d+\/frames\/frame.html— [\d.ms]+/,
/route.continue— [\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+/,
/recordTrace\s+trace-viewer.spec.ts\s+:\d+/,
], { 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 }) => {

View file

@ -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.
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/inMemorySnapshotter.ts'] = ['src/**'];