mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: flatten metadata in trace events (#21214)
This commit is contained in:
parent
e17e0e40f8
commit
22d82b6e1b
@ -26,6 +26,7 @@ import { rewriteErrorMessage } from '../../utils/stackTrace';
|
|||||||
import type { PlaywrightDispatcher } from './playwrightDispatcher';
|
import type { PlaywrightDispatcher } from './playwrightDispatcher';
|
||||||
import { eventsHelper } from '../..//utils/eventsHelper';
|
import { eventsHelper } from '../..//utils/eventsHelper';
|
||||||
import type { RegisteredListener } from '../..//utils/eventsHelper';
|
import type { RegisteredListener } from '../..//utils/eventsHelper';
|
||||||
|
import type * as trace from '@trace/trace';
|
||||||
|
|
||||||
export const dispatcherSymbol = Symbol('dispatcher');
|
export const dispatcherSymbol = Symbol('dispatcher');
|
||||||
const metadataValidator = createMetadataValidator();
|
const metadataValidator = createMetadataValidator();
|
||||||
@ -186,20 +187,15 @@ export class DispatcherConnection {
|
|||||||
|
|
||||||
private _sendMessageToClient(guid: string, type: string, method: string, params: any, sdkObject?: SdkObject) {
|
private _sendMessageToClient(guid: string, type: string, method: string, params: any, sdkObject?: SdkObject) {
|
||||||
if (sdkObject) {
|
if (sdkObject) {
|
||||||
const eventMetadata: CallMetadata = {
|
const event: trace.EventTraceEvent = {
|
||||||
id: `event@${++lastEventId}`,
|
type: 'event',
|
||||||
objectId: sdkObject?.guid,
|
class: type,
|
||||||
pageId: sdkObject?.attribution?.page?.guid,
|
|
||||||
frameId: sdkObject?.attribution?.frame?.guid,
|
|
||||||
startTime: monotonicTime(),
|
|
||||||
endTime: 0,
|
|
||||||
type,
|
|
||||||
method,
|
method,
|
||||||
params: params || {},
|
params: params || {},
|
||||||
log: [],
|
time: monotonicTime(),
|
||||||
snapshots: []
|
pageId: sdkObject?.attribution?.page?.guid,
|
||||||
};
|
};
|
||||||
sdkObject.instrumentation?.onEvent(sdkObject, eventMetadata);
|
sdkObject.instrumentation?.onEvent(sdkObject, event);
|
||||||
}
|
}
|
||||||
this.onmessage({ guid, method, params });
|
this.onmessage({ guid, method, params });
|
||||||
}
|
}
|
||||||
@ -330,5 +326,3 @@ function formatLogRecording(log: string[]): string {
|
|||||||
const rightLength = headerLength - header.length - leftLength;
|
const rightLength = headerLength - header.length - leftLength;
|
||||||
return `\n${'='.repeat(leftLength)}${header}${'='.repeat(rightLength)}\n${log.join('\n')}\n${'='.repeat(headerLength)}`;
|
return `\n${'='.repeat(leftLength)}${header}${'='.repeat(rightLength)}\n${log.join('\n')}\n${'='.repeat(headerLength)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastEventId = 0;
|
|
||||||
|
|||||||
@ -35,6 +35,7 @@ export type Attribution = {
|
|||||||
|
|
||||||
import type { CallMetadata } from '@protocol/callMetadata';
|
import type { CallMetadata } from '@protocol/callMetadata';
|
||||||
export type { CallMetadata } from '@protocol/callMetadata';
|
export type { CallMetadata } from '@protocol/callMetadata';
|
||||||
|
import type * as trace from '@trace/trace';
|
||||||
|
|
||||||
export const kTestSdkObjects = new WeakSet<SdkObject>();
|
export const kTestSdkObjects = new WeakSet<SdkObject>();
|
||||||
|
|
||||||
@ -61,7 +62,7 @@ export interface Instrumentation {
|
|||||||
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
|
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
|
||||||
onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
|
onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
|
||||||
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||||
onEvent(sdkObject: SdkObject, metadata: CallMetadata): void;
|
onEvent(sdkObject: SdkObject, event: trace.EventTraceEvent): void;
|
||||||
onPageOpen(page: Page): void;
|
onPageOpen(page: Page): void;
|
||||||
onPageClose(page: Page): void;
|
onPageClose(page: Page): void;
|
||||||
onBrowserOpen(browser: Browser): void;
|
onBrowserOpen(browser: Browser): void;
|
||||||
@ -73,7 +74,7 @@ export interface InstrumentationListener {
|
|||||||
onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
|
onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
|
||||||
onCallLog?(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
|
onCallLog?(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
|
||||||
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||||
onEvent?(sdkObject: SdkObject, metadata: CallMetadata): void;
|
onEvent?(sdkObject: SdkObject, event: trace.EventTraceEvent): void;
|
||||||
onPageOpen?(page: Page): void;
|
onPageOpen?(page: Page): void;
|
||||||
onPageClose?(page: Page): void;
|
onPageClose?(page: Page): void;
|
||||||
onBrowserOpen?(browser: Browser): void;
|
onBrowserOpen?(browser: Browser): void;
|
||||||
|
|||||||
@ -43,7 +43,7 @@ import type { SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
|
|||||||
import { Snapshotter } from './snapshotter';
|
import { Snapshotter } from './snapshotter';
|
||||||
import { yazl } from '../../../zipBundle';
|
import { yazl } from '../../../zipBundle';
|
||||||
|
|
||||||
const version: VERSION = 3;
|
const version: VERSION = 4;
|
||||||
|
|
||||||
export type TracerOptions = {
|
export type TracerOptions = {
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -341,15 +341,25 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||||||
}
|
}
|
||||||
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 };
|
const event = createActionTraceEvent(metadata);
|
||||||
|
if (event)
|
||||||
this._appendTraceEvent(event);
|
this._appendTraceEvent(event);
|
||||||
this._pendingCalls.delete(metadata.id);
|
this._pendingCalls.delete(metadata.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEvent(sdkObject: SdkObject, metadata: CallMetadata) {
|
onEvent(sdkObject: SdkObject, event: trace.EventTraceEvent) {
|
||||||
if (!sdkObject.attribution.context)
|
if (!sdkObject.attribution.context)
|
||||||
return;
|
return;
|
||||||
const event: trace.ActionTraceEvent = { type: 'event', metadata };
|
if (event.method === '__create__' && event.class === 'ConsoleMessage') {
|
||||||
|
const object: trace.ObjectTraceEvent = {
|
||||||
|
type: 'object',
|
||||||
|
class: event.class,
|
||||||
|
guid: event.params.guid,
|
||||||
|
initializer: event.params.initializer,
|
||||||
|
};
|
||||||
|
this._appendTraceEvent(object);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._appendTraceEvent(event);
|
this._appendTraceEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -467,3 +477,25 @@ function visitTraceEvent(object: any, sha1s: Set<string>): any {
|
|||||||
export function shouldCaptureSnapshot(metadata: CallMetadata): boolean {
|
export function shouldCaptureSnapshot(metadata: CallMetadata): boolean {
|
||||||
return commandsWithTracingSnapshots.has(metadata.type + '.' + metadata.method);
|
return commandsWithTracingSnapshots.has(metadata.type + '.' + metadata.method);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createActionTraceEvent(metadata: CallMetadata): trace.ActionTraceEvent | null {
|
||||||
|
if (metadata.internal || metadata.method.startsWith('tracing'))
|
||||||
|
return null;
|
||||||
|
return {
|
||||||
|
type: 'action',
|
||||||
|
callId: metadata.id,
|
||||||
|
startTime: metadata.startTime,
|
||||||
|
endTime: metadata.endTime,
|
||||||
|
apiName: metadata.apiName || metadata.type + '.' + metadata.method,
|
||||||
|
class: metadata.type,
|
||||||
|
method: metadata.method,
|
||||||
|
params: metadata.params,
|
||||||
|
wallTime: metadata.wallTime || Date.now(),
|
||||||
|
log: metadata.log,
|
||||||
|
snapshots: metadata.snapshots,
|
||||||
|
error: metadata.error?.error,
|
||||||
|
result: metadata.result,
|
||||||
|
point: metadata.point,
|
||||||
|
pageId: metadata.pageId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -32,8 +32,8 @@ export type ContextEntry = {
|
|||||||
pages: PageEntry[];
|
pages: PageEntry[];
|
||||||
resources: ResourceSnapshot[];
|
resources: ResourceSnapshot[];
|
||||||
actions: trace.ActionTraceEvent[];
|
actions: trace.ActionTraceEvent[];
|
||||||
events: trace.ActionTraceEvent[];
|
events: trace.EventTraceEvent[];
|
||||||
objects: { [key: string]: any };
|
initializers: { [key: string]: any };
|
||||||
hasSource: boolean;
|
hasSource: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ export function createEmptyContext(): ContextEntry {
|
|||||||
resources: [],
|
resources: [],
|
||||||
actions: [],
|
actions: [],
|
||||||
events: [],
|
events: [],
|
||||||
objects: {},
|
initializers: {},
|
||||||
hasSource: false
|
hasSource: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,8 +14,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CallMetadata } from '@protocol/callMetadata';
|
|
||||||
import type * as trace from '@trace/trace';
|
import type * as trace from '@trace/trace';
|
||||||
|
import type * as traceV3 from './versions/traceV3';
|
||||||
import { parseClientSideCallMetadata } from '@trace/traceUtils';
|
import { parseClientSideCallMetadata } from '@trace/traceUtils';
|
||||||
import type zip from '@zip.js/zip.js';
|
import type zip from '@zip.js/zip.js';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -87,7 +87,7 @@ export class TraceModel {
|
|||||||
await stacksEntry.getData!(writer);
|
await stacksEntry.getData!(writer);
|
||||||
const metadataMap = parseClientSideCallMetadata(JSON.parse(await writer.getData()));
|
const metadataMap = parseClientSideCallMetadata(JSON.parse(await writer.getData()));
|
||||||
for (const action of this.contextEntry.actions)
|
for (const action of this.contextEntry.actions)
|
||||||
action.metadata.stack = action.metadata.stack || metadataMap.get(action.metadata.id);
|
action.stack = action.stack || metadataMap.get(action.callId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._build();
|
this._build();
|
||||||
@ -117,7 +117,7 @@ export class TraceModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _build() {
|
private _build() {
|
||||||
this.contextEntry!.actions.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
|
this.contextEntry!.actions.sort((a1, a2) => a1.startTime - a2.startTime);
|
||||||
this.contextEntry!.resources = this._snapshotStorage!.resources();
|
this.contextEntry!.resources = this._snapshotStorage!.resources();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,6 +137,8 @@ export class TraceModel {
|
|||||||
if (!line)
|
if (!line)
|
||||||
return;
|
return;
|
||||||
const event = this._modernize(JSON.parse(line));
|
const event = this._modernize(JSON.parse(line));
|
||||||
|
if (!event)
|
||||||
|
return;
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'context-options': {
|
case 'context-options': {
|
||||||
this.contextEntry.browserName = event.browserName;
|
this.contextEntry.browserName = event.browserName;
|
||||||
@ -153,22 +155,15 @@ export class TraceModel {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'action': {
|
case 'action': {
|
||||||
const include = !isTracing(event.metadata) && (!event.metadata.internal || event.metadata.apiName);
|
|
||||||
if (include) {
|
|
||||||
if (!event.metadata.apiName)
|
|
||||||
event.metadata.apiName = event.metadata.type + '.' + event.metadata.method;
|
|
||||||
this.contextEntry!.actions.push(event);
|
this.contextEntry!.actions.push(event);
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'event': {
|
case 'event': {
|
||||||
const metadata = event.metadata;
|
|
||||||
if (metadata.pageId) {
|
|
||||||
if (metadata.method === '__create__')
|
|
||||||
this.contextEntry!.objects[metadata.params.guid] = metadata.params.initializer;
|
|
||||||
else
|
|
||||||
this.contextEntry!.events.push(event);
|
this.contextEntry!.events.push(event);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
case 'object': {
|
||||||
|
this.contextEntry!.initializers[event.guid] = event.initializer;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'resource-snapshot':
|
case 'resource-snapshot':
|
||||||
@ -178,9 +173,13 @@ export class TraceModel {
|
|||||||
this._snapshotStorage!.addFrameSnapshot(event.snapshot);
|
this._snapshotStorage!.addFrameSnapshot(event.snapshot);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (event.type === 'action' || event.type === 'event') {
|
if (event.type === 'action') {
|
||||||
this.contextEntry!.startTime = Math.min(this.contextEntry!.startTime, event.metadata.startTime);
|
this.contextEntry!.startTime = Math.min(this.contextEntry!.startTime, event.startTime);
|
||||||
this.contextEntry!.endTime = Math.max(this.contextEntry!.endTime, event.metadata.endTime);
|
this.contextEntry!.endTime = Math.max(this.contextEntry!.endTime, event.endTime);
|
||||||
|
}
|
||||||
|
if (event.type === 'event') {
|
||||||
|
this.contextEntry!.startTime = Math.min(this.contextEntry!.startTime, event.time);
|
||||||
|
this.contextEntry!.endTime = Math.max(this.contextEntry!.endTime, event.time);
|
||||||
}
|
}
|
||||||
if (event.type === 'screencast-frame') {
|
if (event.type === 'screencast-frame') {
|
||||||
this.contextEntry!.startTime = Math.min(this.contextEntry!.startTime, event.timestamp);
|
this.contextEntry!.startTime = Math.min(this.contextEntry!.startTime, event.timestamp);
|
||||||
@ -237,6 +236,54 @@ export class TraceModel {
|
|||||||
}
|
}
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_modernize_3_to_4(event: traceV3.TraceEvent): trace.TraceEvent | null {
|
||||||
|
if (event.type !== 'action') {
|
||||||
|
return event as traceV3.ContextCreatedTraceEvent |
|
||||||
|
traceV3.ScreencastFrameTraceEvent |
|
||||||
|
traceV3.ResourceSnapshotTraceEvent |
|
||||||
|
traceV3.FrameSnapshotTraceEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = event.metadata;
|
||||||
|
if (metadata.internal || metadata.method.startsWith('tracing'))
|
||||||
|
return null;
|
||||||
|
if (metadata.id.startsWith('event@')) {
|
||||||
|
if (metadata.method === '__create__' && metadata.type === 'ConsoleMessage') {
|
||||||
|
return {
|
||||||
|
type: 'object',
|
||||||
|
class: metadata.type,
|
||||||
|
guid: metadata.params.guid,
|
||||||
|
initializer: metadata.params.initializer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'event',
|
||||||
|
time: metadata.startTime,
|
||||||
|
class: metadata.type,
|
||||||
|
method: metadata.method,
|
||||||
|
params: metadata.params,
|
||||||
|
pageId: metadata.pageId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'action',
|
||||||
|
callId: metadata.id,
|
||||||
|
startTime: metadata.startTime,
|
||||||
|
endTime: metadata.endTime,
|
||||||
|
apiName: metadata.apiName || metadata.type + '.' + metadata.method,
|
||||||
|
class: metadata.type,
|
||||||
|
method: metadata.method,
|
||||||
|
params: metadata.params,
|
||||||
|
wallTime: metadata.wallTime || Date.now(),
|
||||||
|
log: metadata.log,
|
||||||
|
snapshots: metadata.snapshots,
|
||||||
|
error: metadata.error?.error,
|
||||||
|
result: metadata.result,
|
||||||
|
point: metadata.point,
|
||||||
|
pageId: metadata.pageId,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PersistentSnapshotStorage extends BaseSnapshotStorage {
|
export class PersistentSnapshotStorage extends BaseSnapshotStorage {
|
||||||
@ -254,7 +301,3 @@ export class PersistentSnapshotStorage extends BaseSnapshotStorage {
|
|||||||
return writer.getData();
|
return writer.getData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTracing(metadata: CallMetadata): boolean {
|
|
||||||
return metadata.method.startsWith('tracing');
|
|
||||||
}
|
|
||||||
|
|||||||
@ -45,8 +45,8 @@ export const ActionList: React.FC<ActionListProps> = ({
|
|||||||
selectedItem={selectedAction}
|
selectedItem={selectedAction}
|
||||||
onSelected={(action: ActionTraceEvent) => onSelected(action)}
|
onSelected={(action: ActionTraceEvent) => onSelected(action)}
|
||||||
onHighlighted={(action: ActionTraceEvent) => onHighlighted(action)}
|
onHighlighted={(action: ActionTraceEvent) => onHighlighted(action)}
|
||||||
itemKey={(action: ActionTraceEvent) => action.metadata.id}
|
itemKey={(action: ActionTraceEvent) => action.callId}
|
||||||
itemType={(action: ActionTraceEvent) => action.metadata.error?.error?.message ? 'error' : undefined}
|
itemType={(action: ActionTraceEvent) => action.error?.message ? 'error' : undefined}
|
||||||
itemRender={(action: ActionTraceEvent) => renderAction(action, sdkLanguage, setSelectedTab)}
|
itemRender={(action: ActionTraceEvent) => renderAction(action, sdkLanguage, setSelectedTab)}
|
||||||
showNoItemsMessage={true}
|
showNoItemsMessage={true}
|
||||||
></ListView>;
|
></ListView>;
|
||||||
@ -57,17 +57,16 @@ const renderAction = (
|
|||||||
sdkLanguage: Language | undefined,
|
sdkLanguage: Language | undefined,
|
||||||
setSelectedTab: (tab: string) => void
|
setSelectedTab: (tab: string) => void
|
||||||
) => {
|
) => {
|
||||||
const { metadata } = action;
|
|
||||||
const { errors, warnings } = modelUtil.stats(action);
|
const { errors, warnings } = modelUtil.stats(action);
|
||||||
const locator = metadata.params.selector ? asLocator(sdkLanguage || 'javascript', metadata.params.selector) : undefined;
|
const locator = action.params.selector ? asLocator(sdkLanguage || 'javascript', action.params.selector) : undefined;
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className='action-title'>
|
<div className='action-title'>
|
||||||
<span>{metadata.apiName}</span>
|
<span>{action.apiName}</span>
|
||||||
{locator && <div className='action-selector' title={locator}>{locator}</div>}
|
{locator && <div className='action-selector' title={locator}>{locator}</div>}
|
||||||
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
|
{action.method === 'goto' && action.params.url && <div className='action-url' title={action.params.url}>{action.params.url}</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className='action-duration' style={{ flex: 'none' }}>{metadata.endTime ? msToString(metadata.endTime - metadata.startTime) : 'Timed Out'}</div>
|
<div className='action-duration' style={{ flex: 'none' }}>{action.endTime ? msToString(action.endTime - action.startTime) : 'Timed Out'}</div>
|
||||||
<div className='action-icons' onClick={() => setSelectedTab('console')}>
|
<div className='action-icons' onClick={() => setSelectedTab('console')}>
|
||||||
{!!errors && <div className='action-icon'><span className={'codicon codicon-error'}></span><span className="action-icon-value">{errors}</span></div>}
|
{!!errors && <div className='action-icon'><span className={'codicon codicon-error'}></span><span className="action-icon-value">{errors}</span></div>}
|
||||||
{!!warnings && <div className='action-icon'><span className={'codicon codicon-warning'}></span><span className="action-icon-value">{warnings}</span></div>}
|
{!!warnings && <div className='action-icon'><span className={'codicon codicon-warning'}></span><span className="action-icon-value">{warnings}</span></div>}
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CallMetadata } from '@protocol/callMetadata';
|
|
||||||
import type { SerializedValue } from '@protocol/channels';
|
import type { SerializedValue } from '@protocol/channels';
|
||||||
import type { ActionTraceEvent } from '@trace/trace';
|
import type { ActionTraceEvent } from '@trace/trace';
|
||||||
import { msToString } from '@web/uiUtils';
|
import { msToString } from '@web/uiUtils';
|
||||||
@ -30,20 +29,20 @@ export const CallTab: React.FunctionComponent<{
|
|||||||
}> = ({ action, sdkLanguage }) => {
|
}> = ({ action, sdkLanguage }) => {
|
||||||
if (!action)
|
if (!action)
|
||||||
return null;
|
return null;
|
||||||
const logs = action.metadata.log;
|
const logs = action.log;
|
||||||
const error = action.metadata.error?.error?.message;
|
const error = action.error?.message;
|
||||||
const params = { ...action.metadata.params };
|
const params = { ...action.params };
|
||||||
// Strip down the waitForEventInfo data, we never need it.
|
// Strip down the waitForEventInfo data, we never need it.
|
||||||
delete params.info;
|
delete params.info;
|
||||||
const paramKeys = Object.keys(params);
|
const paramKeys = Object.keys(params);
|
||||||
const wallTime = action.metadata.wallTime ? new Date(action.metadata.wallTime).toLocaleString() : null;
|
const wallTime = action.wallTime ? new Date(action.wallTime).toLocaleString() : null;
|
||||||
const duration = action.metadata.endTime ? msToString(action.metadata.endTime - action.metadata.startTime) : 'Timed Out';
|
const duration = action.endTime ? msToString(action.endTime - action.startTime) : 'Timed Out';
|
||||||
return <div className='call-tab'>
|
return <div className='call-tab'>
|
||||||
<div className='call-error' key='error' hidden={!error}>
|
<div className='call-error' key='error' hidden={!error}>
|
||||||
<div className='codicon codicon-issues'/>
|
<div className='codicon codicon-issues'/>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
<div className='call-line'>{action.metadata.apiName}</div>
|
<div className='call-line'>{action.apiName}</div>
|
||||||
{<>
|
{<>
|
||||||
<div className='call-section'>Time</div>
|
<div className='call-section'>Time</div>
|
||||||
{wallTime && <div className='call-line'>wall time:<span className='call-value datetime' title={wallTime}>{wallTime}</span></div>}
|
{wallTime && <div className='call-line'>wall time:<span className='call-value datetime' title={wallTime}>{wallTime}</span></div>}
|
||||||
@ -51,12 +50,12 @@ export const CallTab: React.FunctionComponent<{
|
|||||||
</>}
|
</>}
|
||||||
{ !!paramKeys.length && <div className='call-section'>Parameters</div> }
|
{ !!paramKeys.length && <div className='call-section'>Parameters</div> }
|
||||||
{
|
{
|
||||||
!!paramKeys.length && paramKeys.map((name, index) => renderProperty(propertyToString(action.metadata, name, params[name], sdkLanguage), 'param-' + index))
|
!!paramKeys.length && paramKeys.map((name, index) => renderProperty(propertyToString(action, name, params[name], sdkLanguage), 'param-' + index))
|
||||||
}
|
}
|
||||||
{ !!action.metadata.result && <div className='call-section'>Return value</div> }
|
{ !!action.result && <div className='call-section'>Return value</div> }
|
||||||
{
|
{
|
||||||
!!action.metadata.result && Object.keys(action.metadata.result).map((name, index) =>
|
!!action.result && Object.keys(action.result).map((name, index) =>
|
||||||
renderProperty(propertyToString(action.metadata, name, action.metadata.result[name], sdkLanguage), 'result-' + index)
|
renderProperty(propertyToString(action, name, action.result[name], sdkLanguage), 'result-' + index)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<div className='call-section'>Log</div>
|
<div className='call-section'>Log</div>
|
||||||
@ -90,14 +89,14 @@ function renderProperty(property: Property, key: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function propertyToString(metadata: CallMetadata, name: string, value: any, sdkLanguage: Language | undefined): Property {
|
function propertyToString(event: ActionTraceEvent, name: string, value: any, sdkLanguage: Language | undefined): Property {
|
||||||
const isEval = metadata.method.includes('eval') || metadata.method === 'waitForFunction';
|
const isEval = event.method.includes('eval') || event.method === 'waitForFunction';
|
||||||
if (name === 'eventInit' || name === 'expectedValue' || (name === 'arg' && isEval))
|
if (name === 'eventInit' || name === 'expectedValue' || (name === 'arg' && isEval))
|
||||||
value = parseSerializedValue(value.value, new Array(10).fill({ handle: '<handle>' }));
|
value = parseSerializedValue(value.value, new Array(10).fill({ handle: '<handle>' }));
|
||||||
if ((name === 'value' && isEval) || (name === 'received' && metadata.method === 'expect'))
|
if ((name === 'value' && isEval) || (name === 'received' && event.method === 'expect'))
|
||||||
value = parseSerializedValue(value, new Array(10).fill({ handle: '<handle>' }));
|
value = parseSerializedValue(value, new Array(10).fill({ handle: '<handle>' }));
|
||||||
if (name === 'selector')
|
if (name === 'selector')
|
||||||
return { text: asLocator(sdkLanguage || 'javascript', metadata.params.selector), type: 'locator', name: 'locator' };
|
return { text: asLocator(sdkLanguage || 'javascript', event.params.selector), type: 'locator', name: 'locator' };
|
||||||
const type = typeof value;
|
const type = typeof value;
|
||||||
if (type !== 'object' || value === null)
|
if (type !== 'object' || value === null)
|
||||||
return { text: String(value), type, name };
|
return { text: String(value), type, name };
|
||||||
|
|||||||
@ -29,14 +29,14 @@ export const ConsoleTab: React.FunctionComponent<{
|
|||||||
const entries: { message?: channels.ConsoleMessageInitializer, error?: channels.SerializedError }[] = [];
|
const entries: { message?: channels.ConsoleMessageInitializer, error?: channels.SerializedError }[] = [];
|
||||||
const context = modelUtil.context(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.method !== 'console' && event.method !== 'pageError')
|
||||||
continue;
|
continue;
|
||||||
if (event.metadata.method === 'console') {
|
if (event.method === 'console') {
|
||||||
const { guid } = event.metadata.params.message;
|
const { guid } = event.params.message;
|
||||||
entries.push({ message: context.objects[guid] });
|
entries.push({ message: context.initializers[guid] });
|
||||||
}
|
}
|
||||||
if (event.metadata.method === 'pageError')
|
if (event.method === 'pageError')
|
||||||
entries.push({ error: event.metadata.params.error });
|
entries.push({ error: event.params.error });
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
}, [action]);
|
}, [action]);
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
import type { Language } from '@isomorphic/locatorGenerators';
|
import type { Language } from '@isomorphic/locatorGenerators';
|
||||||
import type { ResourceSnapshot } from '@trace/snapshot';
|
import type { ResourceSnapshot } from '@trace/snapshot';
|
||||||
import type * as trace from '@trace/trace';
|
import type * as trace from '@trace/trace';
|
||||||
import type { ActionTraceEvent } from '@trace/trace';
|
import type { ActionTraceEvent, EventTraceEvent } from '@trace/trace';
|
||||||
import type { ContextEntry, PageEntry } from '../entries';
|
import type { ContextEntry, PageEntry } from '../entries';
|
||||||
|
|
||||||
const contextSymbol = Symbol('context');
|
const contextSymbol = Symbol('context');
|
||||||
@ -35,7 +35,7 @@ export class MultiTraceModel {
|
|||||||
readonly options: trace.BrowserContextEventOptions;
|
readonly options: trace.BrowserContextEventOptions;
|
||||||
readonly pages: PageEntry[];
|
readonly pages: PageEntry[];
|
||||||
readonly actions: trace.ActionTraceEvent[];
|
readonly actions: trace.ActionTraceEvent[];
|
||||||
readonly events: trace.ActionTraceEvent[];
|
readonly events: trace.EventTraceEvent[];
|
||||||
readonly hasSource: boolean;
|
readonly hasSource: boolean;
|
||||||
readonly sdkLanguage: Language | undefined;
|
readonly sdkLanguage: Language | undefined;
|
||||||
readonly testIdAttributeName: string | undefined;
|
readonly testIdAttributeName: string | undefined;
|
||||||
@ -54,11 +54,11 @@ export class MultiTraceModel {
|
|||||||
this.endTime = contexts.map(c => c.endTime).reduce((prev, cur) => Math.max(prev, cur), Number.MIN_VALUE);
|
this.endTime = contexts.map(c => c.endTime).reduce((prev, cur) => Math.max(prev, cur), Number.MIN_VALUE);
|
||||||
this.pages = ([] as PageEntry[]).concat(...contexts.map(c => c.pages));
|
this.pages = ([] as PageEntry[]).concat(...contexts.map(c => c.pages));
|
||||||
this.actions = ([] as ActionTraceEvent[]).concat(...contexts.map(c => c.actions));
|
this.actions = ([] as ActionTraceEvent[]).concat(...contexts.map(c => c.actions));
|
||||||
this.events = ([] as ActionTraceEvent[]).concat(...contexts.map(c => c.events));
|
this.events = ([] as EventTraceEvent[]).concat(...contexts.map(c => c.events));
|
||||||
this.hasSource = contexts.some(c => c.hasSource);
|
this.hasSource = contexts.some(c => c.hasSource);
|
||||||
|
|
||||||
this.actions.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
|
this.actions.sort((a1, a2) => a1.startTime - a2.startTime);
|
||||||
this.events.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
|
this.events.sort((a1, a2) => a1.time - a2.time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,28 +87,28 @@ export function stats(action: ActionTraceEvent): { errors: number, warnings: num
|
|||||||
let warnings = 0;
|
let warnings = 0;
|
||||||
const c = context(action);
|
const c = context(action);
|
||||||
for (const event of eventsForAction(action)) {
|
for (const event of eventsForAction(action)) {
|
||||||
if (event.metadata.method === 'console') {
|
if (event.method === 'console') {
|
||||||
const { guid } = event.metadata.params.message;
|
const { guid } = event.params.message;
|
||||||
const type = c.objects[guid]?.type;
|
const type = c.initializers[guid]?.type;
|
||||||
if (type === 'warning')
|
if (type === 'warning')
|
||||||
++warnings;
|
++warnings;
|
||||||
else if (type === 'error')
|
else if (type === 'error')
|
||||||
++errors;
|
++errors;
|
||||||
}
|
}
|
||||||
if (event.metadata.method === 'pageError')
|
if (event.method === 'pageError')
|
||||||
++errors;
|
++errors;
|
||||||
}
|
}
|
||||||
return { errors, warnings };
|
return { errors, warnings };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function eventsForAction(action: ActionTraceEvent): ActionTraceEvent[] {
|
export function eventsForAction(action: ActionTraceEvent): EventTraceEvent[] {
|
||||||
let result: ActionTraceEvent[] = (action as any)[eventsSymbol];
|
let result: EventTraceEvent[] = (action as any)[eventsSymbol];
|
||||||
if (result)
|
if (result)
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
const nextAction = next(action);
|
const nextAction = next(action);
|
||||||
result = context(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.time >= action.startTime && (!nextAction || event.time < nextAction.startTime);
|
||||||
});
|
});
|
||||||
(action as any)[eventsSymbol] = result;
|
(action as any)[eventsSymbol] = result;
|
||||||
return result;
|
return result;
|
||||||
@ -121,7 +121,7 @@ export function resourcesForAction(action: ActionTraceEvent): ResourceSnapshot[]
|
|||||||
|
|
||||||
const nextAction = next(action);
|
const nextAction = next(action);
|
||||||
result = context(action).resources.filter(resource => {
|
result = context(action).resources.filter(resource => {
|
||||||
return typeof resource._monotonicTime === 'number' && resource._monotonicTime > action.metadata.startTime && (!nextAction || resource._monotonicTime < nextAction.metadata.startTime);
|
return typeof resource._monotonicTime === 'number' && resource._monotonicTime > action.startTime && (!nextAction || resource._monotonicTime < nextAction.startTime);
|
||||||
});
|
});
|
||||||
(action as any)[resourcesSymbol] = result;
|
(action as any)[resourcesSymbol] = result;
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||||||
const [pickerVisible, setPickerVisible] = React.useState(false);
|
const [pickerVisible, setPickerVisible] = React.useState(false);
|
||||||
|
|
||||||
const snapshotMap = new Map<string, { title: string, snapshotName: string }>();
|
const snapshotMap = new Map<string, { title: string, snapshotName: string }>();
|
||||||
for (const snapshot of action?.metadata.snapshots || [])
|
for (const snapshot of action?.snapshots || [])
|
||||||
snapshotMap.set(snapshot.title, snapshot);
|
snapshotMap.set(snapshot.title, snapshot);
|
||||||
const actionSnapshot = snapshotMap.get('action') || snapshotMap.get('after');
|
const actionSnapshot = snapshotMap.get('action') || snapshotMap.get('after');
|
||||||
const snapshots = [actionSnapshot ? { ...actionSnapshot, title: 'action' } : undefined, snapshotMap.get('before'), snapshotMap.get('after')].filter(Boolean) as { title: string, snapshotName: string }[];
|
const snapshots = [actionSnapshot ? { ...actionSnapshot, title: 'action' } : undefined, snapshotMap.get('before'), snapshotMap.get('after')].filter(Boolean) as { title: string, snapshotName: string }[];
|
||||||
@ -58,11 +58,11 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('trace', context(action).traceUrl);
|
params.set('trace', context(action).traceUrl);
|
||||||
params.set('name', snapshot.snapshotName);
|
params.set('name', snapshot.snapshotName);
|
||||||
snapshotUrl = new URL(`snapshot/${action.metadata.pageId}?${params.toString()}`, window.location.href).toString();
|
snapshotUrl = new URL(`snapshot/${action.pageId}?${params.toString()}`, window.location.href).toString();
|
||||||
snapshotInfoUrl = new URL(`snapshotInfo/${action.metadata.pageId}?${params.toString()}`, window.location.href).toString();
|
snapshotInfoUrl = new URL(`snapshotInfo/${action.pageId}?${params.toString()}`, window.location.href).toString();
|
||||||
if (snapshot.snapshotName.includes('action')) {
|
if (snapshot.snapshotName.includes('action')) {
|
||||||
pointX = action.metadata.point?.x;
|
pointX = action.point?.x;
|
||||||
pointY = action.metadata.point?.y;
|
pointY = action.point?.y;
|
||||||
}
|
}
|
||||||
const popoutParams = new URLSearchParams();
|
const popoutParams = new URLSearchParams();
|
||||||
popoutParams.set('r', snapshotUrl);
|
popoutParams.set('r', snapshotUrl);
|
||||||
|
|||||||
@ -44,10 +44,7 @@ export const SourceTab: React.FunctionComponent<{
|
|||||||
const stackInfo = React.useMemo<StackInfo>(() => {
|
const stackInfo = React.useMemo<StackInfo>(() => {
|
||||||
if (!action)
|
if (!action)
|
||||||
return '';
|
return '';
|
||||||
const { metadata } = action;
|
const frames = action.stack || [];
|
||||||
if (!metadata.stack)
|
|
||||||
return '';
|
|
||||||
const frames = metadata.stack;
|
|
||||||
return {
|
return {
|
||||||
frames,
|
frames,
|
||||||
fileContent: new Map(),
|
fileContent: new Map(),
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export const StackTraceView: React.FunctionComponent<{
|
|||||||
selectedFrame: number,
|
selectedFrame: number,
|
||||||
setSelectedFrame: (index: number) => void
|
setSelectedFrame: (index: number) => void
|
||||||
}> = ({ action, setSelectedFrame, selectedFrame }) => {
|
}> = ({ action, setSelectedFrame, selectedFrame }) => {
|
||||||
const frames = action?.metadata.stack || [];
|
const frames = action?.stack || [];
|
||||||
return <ListView
|
return <ListView
|
||||||
dataTestId='stack-trace'
|
dataTestId='stack-trace'
|
||||||
items={frames}
|
items={frames}
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ActionTraceEvent } from '@trace/trace';
|
import type { ActionTraceEvent, EventTraceEvent } from '@trace/trace';
|
||||||
import { msToString } from '@web/uiUtils';
|
import { msToString } from '@web/uiUtils';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { Boundaries } from '../geometry';
|
import type { Boundaries } from '../geometry';
|
||||||
@ -26,7 +26,7 @@ import './timeline.css';
|
|||||||
|
|
||||||
type TimelineBar = {
|
type TimelineBar = {
|
||||||
action?: ActionTraceEvent;
|
action?: ActionTraceEvent;
|
||||||
event?: ActionTraceEvent;
|
event?: EventTraceEvent;
|
||||||
leftPosition: number;
|
leftPosition: number;
|
||||||
rightPosition: number;
|
rightPosition: number;
|
||||||
leftTime: number;
|
leftTime: number;
|
||||||
@ -56,34 +56,34 @@ export const Timeline: React.FunctionComponent<{
|
|||||||
const bars = React.useMemo(() => {
|
const bars = React.useMemo(() => {
|
||||||
const bars: TimelineBar[] = [];
|
const bars: TimelineBar[] = [];
|
||||||
for (const entry of context.actions) {
|
for (const entry of context.actions) {
|
||||||
let detail = trimRight(entry.metadata.params.selector || '', 50);
|
let detail = trimRight(entry.params.selector || '', 50);
|
||||||
if (entry.metadata.method === 'goto')
|
if (entry.method === 'goto')
|
||||||
detail = trimRight(entry.metadata.params.url || '', 50);
|
detail = trimRight(entry.params.url || '', 50);
|
||||||
bars.push({
|
bars.push({
|
||||||
action: entry,
|
action: entry,
|
||||||
leftTime: entry.metadata.startTime,
|
leftTime: entry.startTime,
|
||||||
rightTime: entry.metadata.endTime,
|
rightTime: entry.endTime,
|
||||||
leftPosition: timeToPosition(measure.width, boundaries, entry.metadata.startTime),
|
leftPosition: timeToPosition(measure.width, boundaries, entry.startTime),
|
||||||
rightPosition: timeToPosition(measure.width, boundaries, entry.metadata.endTime),
|
rightPosition: timeToPosition(measure.width, boundaries, entry.endTime),
|
||||||
label: entry.metadata.apiName + ' ' + detail,
|
label: entry.apiName + ' ' + detail,
|
||||||
title: entry.metadata.endTime ? msToString(entry.metadata.endTime - entry.metadata.startTime) : 'Timed Out',
|
title: entry.endTime ? msToString(entry.endTime - entry.startTime) : 'Timed Out',
|
||||||
type: entry.metadata.type + '.' + entry.metadata.method,
|
type: entry.type + '.' + entry.method,
|
||||||
className: `${entry.metadata.type}_${entry.metadata.method}`.toLowerCase()
|
className: `${entry.type}_${entry.method}`.toLowerCase()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const event of context.events) {
|
for (const event of context.events) {
|
||||||
const startTime = event.metadata.startTime;
|
const startTime = event.time;
|
||||||
bars.push({
|
bars.push({
|
||||||
event,
|
event,
|
||||||
leftTime: startTime,
|
leftTime: startTime,
|
||||||
rightTime: startTime,
|
rightTime: startTime,
|
||||||
leftPosition: timeToPosition(measure.width, boundaries, startTime),
|
leftPosition: timeToPosition(measure.width, boundaries, startTime),
|
||||||
rightPosition: timeToPosition(measure.width, boundaries, startTime),
|
rightPosition: timeToPosition(measure.width, boundaries, startTime),
|
||||||
label: event.metadata.method,
|
label: event.method,
|
||||||
title: event.metadata.endTime ? msToString(event.metadata.endTime - event.metadata.startTime) : 'Timed Out',
|
title: '0',
|
||||||
type: event.metadata.type + '.' + event.metadata.method,
|
type: event.class + '.' + event.method,
|
||||||
className: `${event.metadata.type}_${event.metadata.method}`.toLowerCase()
|
className: `${event.class}_${event.method}`.toLowerCase()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return bars;
|
return bars;
|
||||||
@ -237,5 +237,5 @@ function trimRight(s: string, maxLength: number): string {
|
|||||||
|
|
||||||
const kBarHeight = 11;
|
const kBarHeight = 11;
|
||||||
function barTop(bar: TimelineBar): number {
|
function barTop(bar: TimelineBar): number {
|
||||||
return bar.event ? 22 : (bar.action?.metadata.method === 'waitForEventInfo' ? 0 : 11);
|
return bar.event ? 22 : (bar.action?.method === 'waitForEventInfo' ? 0 : 11);
|
||||||
}
|
}
|
||||||
|
|||||||
162
packages/trace-viewer/src/versions/traceV3.ts
Normal file
162
packages/trace-viewer/src/versions/traceV3.ts
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Entry as ResourceSnapshot } from '../../../trace/src/har';
|
||||||
|
|
||||||
|
type SerializedValue = {
|
||||||
|
n?: number,
|
||||||
|
b?: boolean,
|
||||||
|
s?: string,
|
||||||
|
v?: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0',
|
||||||
|
d?: string,
|
||||||
|
u?: string,
|
||||||
|
r?: {
|
||||||
|
p: string,
|
||||||
|
f: string,
|
||||||
|
},
|
||||||
|
a?: SerializedValue[],
|
||||||
|
o?: {
|
||||||
|
k: string,
|
||||||
|
v: SerializedValue,
|
||||||
|
}[],
|
||||||
|
h?: number,
|
||||||
|
id?: number,
|
||||||
|
ref?: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Point = {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
type StackFrame = {
|
||||||
|
file: string,
|
||||||
|
line: number,
|
||||||
|
column: number,
|
||||||
|
function?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
type SerializedError = {
|
||||||
|
error?: {
|
||||||
|
message: string,
|
||||||
|
name: string,
|
||||||
|
stack?: string,
|
||||||
|
},
|
||||||
|
value?: SerializedValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
type CallMetadata = {
|
||||||
|
id: string;
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
pauseStartTime?: number;
|
||||||
|
pauseEndTime?: number;
|
||||||
|
type: string;
|
||||||
|
method: string;
|
||||||
|
params: any;
|
||||||
|
apiName?: string;
|
||||||
|
internal?: boolean;
|
||||||
|
isServerSide?: boolean;
|
||||||
|
wallTime?: number;
|
||||||
|
location?: { file: string, line?: number, column?: number };
|
||||||
|
log: string[];
|
||||||
|
afterSnapshot?: string;
|
||||||
|
snapshots: { title: string, snapshotName: string }[];
|
||||||
|
error?: SerializedError;
|
||||||
|
result?: any;
|
||||||
|
point?: Point;
|
||||||
|
objectId?: string;
|
||||||
|
pageId?: string;
|
||||||
|
frameId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NodeSnapshot =
|
||||||
|
string |
|
||||||
|
[ [number, number] ] |
|
||||||
|
[ string ] |
|
||||||
|
[ string, { [attr: string]: string }, ...any ];
|
||||||
|
|
||||||
|
|
||||||
|
export type ResourceOverride = {
|
||||||
|
url: string,
|
||||||
|
sha1?: string,
|
||||||
|
ref?: number
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FrameSnapshot = {
|
||||||
|
snapshotName?: string,
|
||||||
|
pageId: string,
|
||||||
|
frameId: string,
|
||||||
|
frameUrl: string,
|
||||||
|
timestamp: number,
|
||||||
|
collectionTime: number,
|
||||||
|
doctype?: string,
|
||||||
|
html: NodeSnapshot,
|
||||||
|
resourceOverrides: ResourceOverride[],
|
||||||
|
viewport: { width: number, height: number },
|
||||||
|
isMainFrame: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type BrowserContextEventOptions = {
|
||||||
|
viewport?: { width: number, height: number },
|
||||||
|
deviceScaleFactor?: number,
|
||||||
|
isMobile?: boolean,
|
||||||
|
userAgent?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ContextCreatedTraceEvent = {
|
||||||
|
version: number,
|
||||||
|
type: 'context-options',
|
||||||
|
browserName: string,
|
||||||
|
platform: string,
|
||||||
|
wallTime: number,
|
||||||
|
title?: string,
|
||||||
|
options: BrowserContextEventOptions,
|
||||||
|
sdkLanguage?: 'javascript' | 'python' | 'java' | 'csharp',
|
||||||
|
testIdAttributeName?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScreencastFrameTraceEvent = {
|
||||||
|
type: 'screencast-frame',
|
||||||
|
pageId: string,
|
||||||
|
sha1: string,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
timestamp: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ActionTraceEvent = {
|
||||||
|
type: 'action' | 'event',
|
||||||
|
metadata: CallMetadata & { stack?: StackFrame[] },
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResourceSnapshotTraceEvent = {
|
||||||
|
type: 'resource-snapshot',
|
||||||
|
snapshot: ResourceSnapshot,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FrameSnapshotTraceEvent = {
|
||||||
|
type: 'frame-snapshot',
|
||||||
|
snapshot: FrameSnapshot,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TraceEvent =
|
||||||
|
ContextCreatedTraceEvent |
|
||||||
|
ScreencastFrameTraceEvent |
|
||||||
|
ActionTraceEvent |
|
||||||
|
ResourceSnapshotTraceEvent |
|
||||||
|
FrameSnapshotTraceEvent;
|
||||||
@ -14,15 +14,14 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CallMetadata } from '@protocol/callMetadata';
|
import type { Point, SerializedError, StackFrame } from '@protocol/channels';
|
||||||
import type { StackFrame } from '@protocol/channels';
|
|
||||||
import type { Language } from '../../playwright-core/src/server/isomorphic/locatorGenerators';
|
import type { Language } from '../../playwright-core/src/server/isomorphic/locatorGenerators';
|
||||||
import type { FrameSnapshot, ResourceSnapshot } from './snapshot';
|
import type { FrameSnapshot, ResourceSnapshot } from './snapshot';
|
||||||
|
|
||||||
export type Size = { width: number, height: number };
|
export type Size = { width: number, height: number };
|
||||||
|
|
||||||
// Make sure you add _modernize_N_to_N1(event: any) to traceModel.ts.
|
// Make sure you add _modernize_N_to_N1(event: any) to traceModel.ts.
|
||||||
export type VERSION = 3;
|
export type VERSION = 4;
|
||||||
|
|
||||||
export type BrowserContextEventOptions = {
|
export type BrowserContextEventOptions = {
|
||||||
viewport?: Size,
|
viewport?: Size,
|
||||||
@ -53,8 +52,38 @@ export type ScreencastFrameTraceEvent = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ActionTraceEvent = {
|
export type ActionTraceEvent = {
|
||||||
type: 'action' | 'event',
|
type: 'action',
|
||||||
metadata: CallMetadata & { stack?: StackFrame[] },
|
callId: string;
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
apiName: string;
|
||||||
|
class: string;
|
||||||
|
method: string;
|
||||||
|
params: any;
|
||||||
|
wallTime: number;
|
||||||
|
log: string[];
|
||||||
|
snapshots: { title: string, snapshotName: string }[];
|
||||||
|
stack?: StackFrame[];
|
||||||
|
error?: SerializedError['error'];
|
||||||
|
result?: any;
|
||||||
|
point?: Point;
|
||||||
|
pageId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EventTraceEvent = {
|
||||||
|
type: 'event',
|
||||||
|
time: number;
|
||||||
|
class: string;
|
||||||
|
method: string;
|
||||||
|
params: any;
|
||||||
|
pageId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ObjectTraceEvent = {
|
||||||
|
type: 'object';
|
||||||
|
class: string;
|
||||||
|
initializer: any;
|
||||||
|
guid: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ResourceSnapshotTraceEvent = {
|
export type ResourceSnapshotTraceEvent = {
|
||||||
@ -71,5 +100,7 @@ export type TraceEvent =
|
|||||||
ContextCreatedTraceEvent |
|
ContextCreatedTraceEvent |
|
||||||
ScreencastFrameTraceEvent |
|
ScreencastFrameTraceEvent |
|
||||||
ActionTraceEvent |
|
ActionTraceEvent |
|
||||||
|
EventTraceEvent |
|
||||||
|
ObjectTraceEvent |
|
||||||
ResourceSnapshotTraceEvent |
|
ResourceSnapshotTraceEvent |
|
||||||
FrameSnapshotTraceEvent;
|
FrameSnapshotTraceEvent;
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import type { Frame, Page } from 'playwright-core';
|
|||||||
import { ZipFile } from '../../packages/playwright-core/lib/utils/zipFile';
|
import { ZipFile } from '../../packages/playwright-core/lib/utils/zipFile';
|
||||||
import type { StackFrame } from '@protocol/channels';
|
import type { StackFrame } from '@protocol/channels';
|
||||||
import { parseClientSideCallMetadata } from '../../packages/trace/src/traceUtils';
|
import { parseClientSideCallMetadata } from '../../packages/trace/src/traceUtils';
|
||||||
|
import type { ActionTraceEvent } from '../../packages/trace/src/trace';
|
||||||
|
|
||||||
export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> {
|
export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> {
|
||||||
const handle = await page.evaluateHandle(async ({ frameId, url }) => {
|
const handle = await page.evaluateHandle(async ({ frameId, url }) => {
|
||||||
@ -28,20 +29,20 @@ export async function attachFrame(page: Page, frameId: string, url: string): Pro
|
|||||||
await new Promise(x => frame.onload = x);
|
await new Promise(x => frame.onload = x);
|
||||||
return frame;
|
return frame;
|
||||||
}, { frameId, url });
|
}, { frameId, url });
|
||||||
return handle.asElement().contentFrame();
|
return handle.asElement().contentFrame() as Promise<Frame>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function detachFrame(page: Page, frameId: string) {
|
export async function detachFrame(page: Page, frameId: string) {
|
||||||
await page.evaluate(frameId => {
|
await page.evaluate(frameId => {
|
||||||
document.getElementById(frameId).remove();
|
document.getElementById(frameId)!.remove();
|
||||||
}, frameId);
|
}, frameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyViewport(page: Page, width: number, height: number) {
|
export async function verifyViewport(page: Page, width: number, height: number) {
|
||||||
// `expect` may clash in test runner tests if imported eagerly.
|
// `expect` may clash in test runner tests if imported eagerly.
|
||||||
const { expect } = require('@playwright/test');
|
const { expect } = require('@playwright/test');
|
||||||
expect(page.viewportSize().width).toBe(width);
|
expect(page.viewportSize()!.width).toBe(width);
|
||||||
expect(page.viewportSize().height).toBe(height);
|
expect(page.viewportSize()!.height).toBe(height);
|
||||||
expect(await page.evaluate('window.innerWidth')).toBe(width);
|
expect(await page.evaluate('window.innerWidth')).toBe(width);
|
||||||
expect(await page.evaluate('window.innerHeight')).toBe(height);
|
expect(await page.evaluate('window.innerHeight')).toBe(height);
|
||||||
}
|
}
|
||||||
@ -100,18 +101,18 @@ export async function parseTrace(file: string): Promise<{ events: any[], resourc
|
|||||||
resources.set(entry, await zipFS.read(entry));
|
resources.set(entry, await zipFS.read(entry));
|
||||||
zipFS.close();
|
zipFS.close();
|
||||||
|
|
||||||
const events = [];
|
const events: any[] = [];
|
||||||
for (const line of resources.get('trace.trace').toString().split('\n')) {
|
for (const line of resources.get('trace.trace')!.toString().split('\n')) {
|
||||||
if (line)
|
if (line)
|
||||||
events.push(JSON.parse(line));
|
events.push(JSON.parse(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const line of resources.get('trace.network').toString().split('\n')) {
|
for (const line of resources.get('trace.network')!.toString().split('\n')) {
|
||||||
if (line)
|
if (line)
|
||||||
events.push(JSON.parse(line));
|
events.push(JSON.parse(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
const stacks = parseClientSideCallMetadata(JSON.parse(resources.get('trace.stacks').toString()));
|
const stacks = parseClientSideCallMetadata(JSON.parse(resources.get('trace.stacks')!.toString()));
|
||||||
return {
|
return {
|
||||||
events,
|
events,
|
||||||
resources,
|
resources,
|
||||||
@ -120,11 +121,11 @@ export async function parseTrace(file: string): Promise<{ events: any[], resourc
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventsToActions(events: any[]): string[] {
|
function eventsToActions(events: ActionTraceEvent[]): string[] {
|
||||||
// Trace viewer only shows non-internal non-tracing actions.
|
// Trace viewer only shows non-internal non-tracing actions.
|
||||||
return events.filter(e => e.type === 'action' && !e.metadata.internal && !e.metadata.method.startsWith('tracing'))
|
return events.filter(e => e.type === 'action')
|
||||||
.sort((a, b) => a.metadata.startTime - b.metadata.startTime)
|
.sort((a, b) => a.startTime - b.startTime)
|
||||||
.map(e => e.metadata.apiName);
|
.map(e => e.apiName);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseHar(file: string): Promise<Map<string, Buffer>> {
|
export async function parseHar(file: string): Promise<Map<string, Buffer>> {
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import path from 'path';
|
|||||||
import { browserTest, contextTest as test, expect } from '../config/browserTest';
|
import { browserTest, contextTest as test, expect } from '../config/browserTest';
|
||||||
import { parseTrace } from '../config/utils';
|
import { parseTrace } from '../config/utils';
|
||||||
import type { StackFrame } from '@protocol/channels';
|
import type { StackFrame } from '@protocol/channels';
|
||||||
|
import type { ActionTraceEvent } from '../../packages/trace/src/trace';
|
||||||
|
|
||||||
test.skip(({ trace }) => trace === 'on');
|
test.skip(({ trace }) => trace === 'on');
|
||||||
|
|
||||||
@ -111,9 +112,9 @@ test('should not include buffers in the trace', async ({ context, page, server,
|
|||||||
await page.screenshot();
|
await page.screenshot();
|
||||||
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
||||||
const { events } = await parseTrace(testInfo.outputPath('trace.zip'));
|
const { events } = await parseTrace(testInfo.outputPath('trace.zip'));
|
||||||
const screenshotEvent = events.find(e => e.type === 'action' && e.metadata.apiName === 'page.screenshot');
|
const screenshotEvent = events.find(e => e.type === 'action' && e.apiName === 'page.screenshot');
|
||||||
expect(screenshotEvent.metadata.snapshots.length).toBe(2);
|
expect(screenshotEvent.snapshots.length).toBe(2);
|
||||||
expect(screenshotEvent.metadata.result).toEqual({});
|
expect(screenshotEvent.result).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should exclude internal pages', async ({ browserName, context, page, server }, testInfo) => {
|
test('should exclude internal pages', async ({ browserName, context, page, server }, testInfo) => {
|
||||||
@ -127,7 +128,7 @@ test('should exclude internal pages', async ({ browserName, context, page, serve
|
|||||||
const trace = await parseTrace(testInfo.outputPath('trace.zip'));
|
const trace = await parseTrace(testInfo.outputPath('trace.zip'));
|
||||||
const pageIds = new Set();
|
const pageIds = new Set();
|
||||||
trace.events.forEach(e => {
|
trace.events.forEach(e => {
|
||||||
const pageId = e.metadata?.pageId;
|
const pageId = e.pageId;
|
||||||
if (pageId)
|
if (pageId)
|
||||||
pageIds.add(pageId);
|
pageIds.add(pageId);
|
||||||
});
|
});
|
||||||
@ -139,7 +140,7 @@ test('should include context API requests', async ({ browserName, context, page,
|
|||||||
await page.request.post(server.PREFIX + '/simple.json', { data: { foo: 'bar' } });
|
await page.request.post(server.PREFIX + '/simple.json', { data: { foo: 'bar' } });
|
||||||
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
||||||
const { events } = await parseTrace(testInfo.outputPath('trace.zip'));
|
const { events } = await parseTrace(testInfo.outputPath('trace.zip'));
|
||||||
const postEvent = events.find(e => e.metadata?.apiName === 'apiRequestContext.post');
|
const postEvent = events.find(e => e.apiName === 'apiRequestContext.post');
|
||||||
expect(postEvent).toBeTruthy();
|
expect(postEvent).toBeTruthy();
|
||||||
const harEntry = events.find(e => e.type === 'resource-snapshot');
|
const harEntry = events.find(e => e.type === 'resource-snapshot');
|
||||||
expect(harEntry).toBeTruthy();
|
expect(harEntry).toBeTruthy();
|
||||||
@ -350,9 +351,9 @@ test('should include interrupted actions', async ({ context, page, server }, tes
|
|||||||
await context.close();
|
await context.close();
|
||||||
|
|
||||||
const { events } = await parseTrace(testInfo.outputPath('trace.zip'));
|
const { events } = await parseTrace(testInfo.outputPath('trace.zip'));
|
||||||
const clickEvent = events.find(e => e.metadata?.apiName === 'page.click');
|
const clickEvent = events.find(e => e.apiName === 'page.click');
|
||||||
expect(clickEvent).toBeTruthy();
|
expect(clickEvent).toBeTruthy();
|
||||||
expect(clickEvent.metadata.error.error.message).toBe('Action was interrupted');
|
expect(clickEvent.error.message).toBe('Action was interrupted');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw when starting with different options', async ({ context }) => {
|
test('should throw when starting with different options', async ({ context }) => {
|
||||||
@ -395,8 +396,8 @@ test('should work with multiple chunks', async ({ context, page, server }, testI
|
|||||||
'page.click',
|
'page.click',
|
||||||
'page.click',
|
'page.click',
|
||||||
]);
|
]);
|
||||||
expect(trace1.events.find(e => e.metadata?.apiName === 'page.click' && !!e.metadata.error)).toBeTruthy();
|
expect(trace1.events.find(e => e.apiName === 'page.click' && !!e.error)).toBeTruthy();
|
||||||
expect(trace1.events.find(e => e.metadata?.apiName === 'page.click' && e.metadata?.error?.error?.message === 'Action was interrupted')).toBeTruthy();
|
expect(trace1.events.find(e => e.apiName === 'page.click' && e.error?.message === 'Action was interrupted')).toBeTruthy();
|
||||||
expect(trace1.events.some(e => e.type === 'frame-snapshot')).toBeTruthy();
|
expect(trace1.events.some(e => e.type === 'frame-snapshot')).toBeTruthy();
|
||||||
expect(trace1.events.some(e => e.type === 'resource-snapshot' && e.snapshot.request.url.endsWith('style.css'))).toBeTruthy();
|
expect(trace1.events.some(e => e.type === 'resource-snapshot' && e.snapshot.request.url.endsWith('style.css'))).toBeTruthy();
|
||||||
|
|
||||||
@ -472,7 +473,7 @@ test('should hide internal stack frames', async ({ context, page }, testInfo) =>
|
|||||||
await context.tracing.stop({ path: tracePath });
|
await context.tracing.stop({ path: tracePath });
|
||||||
|
|
||||||
const trace = await parseTrace(tracePath);
|
const trace = await parseTrace(tracePath);
|
||||||
const actions = trace.events.filter(e => e.type === 'action' && !e.metadata.apiName.startsWith('tracing.'));
|
const actions = trace.events.filter(e => e.type === 'action' && !e.apiName.startsWith('tracing.'));
|
||||||
expect(actions).toHaveLength(4);
|
expect(actions).toHaveLength(4);
|
||||||
for (const action of actions)
|
for (const action of actions)
|
||||||
expect(relativeStack(action, trace.stacks)).toEqual(['tracing.spec.ts']);
|
expect(relativeStack(action, trace.stacks)).toEqual(['tracing.spec.ts']);
|
||||||
@ -493,7 +494,7 @@ test('should hide internal stack frames in expect', async ({ context, page }, te
|
|||||||
await context.tracing.stop({ path: tracePath });
|
await context.tracing.stop({ path: tracePath });
|
||||||
|
|
||||||
const trace = await parseTrace(tracePath);
|
const trace = await parseTrace(tracePath);
|
||||||
const actions = trace.events.filter(e => e.type === 'action' && !e.metadata.apiName.startsWith('tracing.'));
|
const actions = trace.events.filter(e => e.type === 'action' && !e.apiName.startsWith('tracing.'));
|
||||||
expect(actions).toHaveLength(5);
|
expect(actions).toHaveLength(5);
|
||||||
for (const action of actions)
|
for (const action of actions)
|
||||||
expect(relativeStack(action, trace.stacks)).toEqual(['tracing.spec.ts']);
|
expect(relativeStack(action, trace.stacks)).toEqual(['tracing.spec.ts']);
|
||||||
@ -606,7 +607,7 @@ function expectBlue(pixels: Buffer, offset: number) {
|
|||||||
expect(a).toBe(255);
|
expect(a).toBe(255);
|
||||||
}
|
}
|
||||||
|
|
||||||
function relativeStack(action: any, stacks: Map<string, StackFrame[]>): string[] {
|
function relativeStack(action: ActionTraceEvent, stacks: Map<string, StackFrame[]>): string[] {
|
||||||
const stack = stacks.get(action.metadata.id) || [];
|
const stack = stacks.get(action.callId) || [];
|
||||||
return stack.map(f => f.file.replace(__dirname + path.sep, ''));
|
return stack.map(f => f.file.replace(__dirname + path.sep, ''));
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user