From e31b96cc267f724eb3322308ba5a96f4af83d34f Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 15 Oct 2021 14:22:49 -0800 Subject: [PATCH] feat(tracing): make context.request appear in the trace (#9555) --- .../playwright-core/src/client/android.ts | 2 +- .../src/client/browserContext.ts | 4 +- .../src/client/channelOwner.ts | 16 +- .../playwright-core/src/client/electron.ts | 2 +- packages/playwright-core/src/client/frame.ts | 14 +- .../playwright-core/src/client/network.ts | 20 ++- packages/playwright-core/src/client/page.ts | 12 +- packages/playwright-core/src/client/waiter.ts | 13 +- .../src/server/browserContext.ts | 6 +- .../src/server/trace/common/traceEvents.ts | 1 - .../src/server/trace/recorder/tracing.ts | 8 +- .../playwright-core/src/utils/stackTrace.ts | 2 +- .../src/web/traceViewer/entries.ts | 58 +++++++ .../src/web/traceViewer/traceModel.ts | 149 ++---------------- .../src/web/traceViewer/ui/consoleTab.tsx | 4 +- .../src/web/traceViewer/ui/filmStrip.tsx | 2 +- .../src/web/traceViewer/ui/modelUtil.ts | 31 ++-- .../src/web/traceViewer/ui/timeline.tsx | 58 ++++--- .../src/web/traceViewer/ui/workbench.tsx | 26 +-- .../playwright-test/src/reporters/html.ts | 3 +- tests/browsercontext-fetch.spec.ts | 2 +- tests/trace-viewer/trace-viewer.spec.ts | 10 +- utils/check_deps.js | 2 +- 23 files changed, 177 insertions(+), 268 deletions(-) create mode 100644 packages/playwright-core/src/web/traceViewer/entries.ts diff --git a/packages/playwright-core/src/client/android.ts b/packages/playwright-core/src/client/android.ts index 2eee709783..9364a0211c 100644 --- a/packages/playwright-core/src/client/android.ts +++ b/packages/playwright-core/src/client/android.ts @@ -238,7 +238,7 @@ export class AndroidDevice extends ChannelOwner { 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')); diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 341c9e4317..53018929c6 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -151,7 +151,7 @@ export class BrowserContext extends ChannelOwner {}); + route._internalContinue(); } else { this._routes = this._routes.filter(route => !route.expired()); } @@ -293,7 +293,7 @@ export class BrowserContext extends ChannelOwner { 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')); diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index 0793519282..b13f63f78f 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -82,7 +82,7 @@ export abstract class ChannelOwner { 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(func: (channel: C, stackTrace: ParsedStackTrace) => Promise, logger?: Logger): Promise { + async _wrapApiCall(func: (channel: C, stackTrace: ParsedStackTrace) => Promise, logger?: Logger, isInternal?: boolean): Promise { logger = logger || this._logger; const stackTrace = captureStackTrace(); const { apiName, frameTexts } = stackTrace; @@ -106,23 +106,25 @@ export abstract class ChannelOwner 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\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; } } diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index d171daa00e..0522fe7ad0 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -108,7 +108,7 @@ export class ElectronApplication extends ChannelOwner { 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')); diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index 6e2e2cb83b..f85827a917 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -94,8 +94,8 @@ export class Frame extends ChannelOwner { - 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 { - const waiter = this._setupNavigationWaiter(options); + return this._page!._wrapApiCall(async (channel: channels.PageChannel) => { + const waiter = this._setupNavigationWaiter(channel, options); await waiter.waitForEvent(this._eventEmitter, 'loadstate', s => { waiter.log(` "${s}" event fired`); return s === state; diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index a1d7ed36fa..a4535a5b89 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -167,6 +167,12 @@ export class Request extends ChannelOwner { + 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; - async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: InterceptResponse): Promise; - async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: boolean): Promise { + 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; + async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: InterceptResponse, isInternal?: boolean): Promise; + async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: boolean, isInternal?: boolean): Promise { 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 { @@ -585,7 +595,7 @@ export class WebSocket extends ChannelOwner { 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')); diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index c4468643bc..7c2094fda0 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -369,7 +369,7 @@ export class Page extends ChannelOwner { - 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 { + private async _waitForEvent(channel: channels.EventTargetChannel, event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise { 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}"`); diff --git a/packages/playwright-core/src/client/waiter.ts b/packages/playwright-core/src/client/waiter.ts index b240055741..b066c30af6 100644 --- a/packages/playwright-core/src/client/waiter.ts +++ b/packages/playwright-core/src/client/waiter.ts @@ -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, 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, event: string) { - return new Waiter(channelOwner, event); + static createForEvent(channel: channels.EventTargetChannel, event: string) { + return new Waiter(channel, event); } async waitForEvent(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean | Promise): Promise { diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index b267cc06b2..4d79171760 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -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); diff --git a/packages/playwright-core/src/server/trace/common/traceEvents.ts b/packages/playwright-core/src/server/trace/common/traceEvents.ts index 7dbf3a70a7..34723a69cc 100644 --- a/packages/playwright-core/src/server/trace/common/traceEvents.ts +++ b/packages/playwright-core/src/server/trace/common/traceEvents.ts @@ -45,7 +45,6 @@ export type ScreencastFrameTraceEvent = { export type ActionTraceEvent = { type: 'action' | 'event', - hasSnapshot: boolean, metadata: CallMetadata, }; diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index e9478b613f..fe76272587 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -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); } diff --git a/packages/playwright-core/src/utils/stackTrace.ts b/packages/playwright-core/src/utils/stackTrace.ts index 5fdf5977a4..05bedcf724 100644 --- a/packages/playwright-core/src/utils/stackTrace.ts +++ b/packages/playwright-core/src/utils/stackTrace.ts @@ -38,7 +38,7 @@ export type ParsedStackTrace = { allFrames: StackFrame[]; frames: StackFrame[]; frameTexts: string[]; - apiName: string; + apiName: string | undefined; }; export function captureStackTrace(): ParsedStackTrace { diff --git a/packages/playwright-core/src/web/traceViewer/entries.ts b/packages/playwright-core/src/web/traceViewer/entries.ts new file mode 100644 index 0000000000..878bad40b1 --- /dev/null +++ b/packages/playwright-core/src/web/traceViewer/entries.ts @@ -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: '', + }, + pages: [], + resources: [], + actions: [], + events: [], + objects: {}, + }; +} diff --git a/packages/playwright-core/src/web/traceViewer/traceModel.ts b/packages/playwright-core/src/web/traceViewer/traceModel.ts index 0c731e0d9e..3f5878aa3f 100644 --- a/packages/playwright-core/src/web/traceViewer/traceModel.ts +++ b/packages/playwright-core/src/web/traceViewer/traceModel.ts @@ -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; @@ -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'); +} diff --git a/packages/playwright-core/src/web/traceViewer/ui/consoleTab.tsx b/packages/playwright-core/src/web/traceViewer/ui/consoleTab.tsx index c3dc58c4bb..f6ac938b47 100644 --- a/packages/playwright-core/src/web/traceViewer/ui/consoleTab.tsx +++ b/packages/playwright-core/src/web/traceViewer/ui/consoleTab.tsx @@ -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 }); diff --git a/packages/playwright-core/src/web/traceViewer/ui/filmStrip.tsx b/packages/playwright-core/src/web/traceViewer/ui/filmStrip.tsx index e8ea376298..9fcf5ce68e 100644 --- a/packages/playwright-core/src/web/traceViewer/ui/filmStrip.tsx +++ b/packages/playwright-core/src/web/traceViewer/ui/filmStrip.tsx @@ -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 }; diff --git a/packages/playwright-core/src/web/traceViewer/ui/modelUtil.ts b/packages/playwright-core/src/web/traceViewer/ui/modelUtil.ts index e14e87f7dc..dcc416e1e9 100644 --- a/packages/playwright-core/src/web/traceViewer/ui/modelUtil.ts +++ b/packages/playwright-core/src/web/traceViewer/ui/modelUtil.ts @@ -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; diff --git a/packages/playwright-core/src/web/traceViewer/ui/timeline.tsx b/packages/playwright-core/src/web/traceViewer/ui/timeline.tsx index d984b1d4b1..82c0b10140 100644 --- a/packages/playwright-core/src/web/traceViewer/ui/timeline.tsx +++ b/packages/playwright-core/src/web/traceViewer/ui/timeline.tsx @@ -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]); diff --git a/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx b/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx index c43439a795..df8c72fa60 100644 --- a/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx +++ b/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx @@ -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}/> { @@ -111,17 +104,4 @@ export const Workbench: React.FunctionComponent<{ ; }; -const now = performance.now(); -const emptyContext: ContextEntry = { - startTime: now, - endTime: now, - browserName: '', - options: { - deviceScaleFactor: 1, - isMobile: false, - viewport: { width: 1280, height: 800 }, - _debugName: '', - }, - pages: [], - resources: [], -}; +const emptyContext = createEmptyContext(); diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts index d9b104e1f3..bb27f04e71 100644 --- a/packages/playwright-test/src/reporters/html.ts +++ b/packages/playwright-test/src/reporters/html.ts @@ -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(() => {}); diff --git a/tests/browsercontext-fetch.spec.ts b/tests/browsercontext-fetch.spec.ts index 31c5f955f8..c8902dd46d 100644 --- a/tests/browsercontext-fetch.spec.ts +++ b/tests/browsercontext-fetch.spec.ts @@ -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'); diff --git a/tests/trace-viewer/trace-viewer.spec.ts b/tests/trace-viewer/trace-viewer.spec.ts index 432ebfc319..061845ba26 100644 --- a/tests/trace-viewer/trace-viewer.spec.ts +++ b/tests/trace-viewer/trace-viewer.spec.ts @@ -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,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 }) => { diff --git a/utils/check_deps.js b/utils/check_deps.js index 70ed075032..713716e5d0 100644 --- a/utils/check_deps.js +++ b/utils/check_deps.js @@ -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/**'];