mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-05 02:02:05 +03:00
feat(trace-viewer): add nicer params rendering (#7448)
This commit is contained in:
parent
444d1eb51a
commit
f52a53e21e
@ -20,10 +20,3 @@ export type Rect = Size & Point;
|
||||
export type Quad = [ Point, Point, Point, Point ];
|
||||
export type URLMatch = string | RegExp | ((url: URL) => boolean);
|
||||
export type TimeoutOptions = { timeout?: number };
|
||||
|
||||
export type StackFrame = {
|
||||
file: string,
|
||||
line?: number,
|
||||
column?: number,
|
||||
function?: string,
|
||||
};
|
||||
|
@ -22,7 +22,6 @@ import { assert, debugAssert, isUnderTest, monotonicTime } from '../utils/utils'
|
||||
import { tOptional } from '../protocol/validatorPrimitives';
|
||||
import { kBrowserOrContextClosedError } from '../utils/errors';
|
||||
import { CallMetadata, SdkObject } from '../server/instrumentation';
|
||||
import { StackFrame } from '../common/types';
|
||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||
|
||||
export const dispatcherSymbol = Symbol('dispatcher');
|
||||
@ -133,7 +132,7 @@ export class DispatcherConnection {
|
||||
private _rootDispatcher: Root;
|
||||
onmessage = (message: object) => {};
|
||||
private _validateParams: (type: string, method: string, params: any) => any;
|
||||
private _validateMetadata: (metadata: any) => { stack?: StackFrame[] };
|
||||
private _validateMetadata: (metadata: any) => { stack?: channels.StackFrame[] };
|
||||
private _waitOperations = new Map<string, CallMetadata>();
|
||||
|
||||
sendMessageToClient(guid: string, type: string, method: string, params: any, sdkObject?: SdkObject) {
|
||||
|
38
src/protocol/callMetadata.ts
Normal file
38
src/protocol/callMetadata.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 { Point, StackFrame, SerializedError } from './channels';
|
||||
|
||||
export type CallMetadata = {
|
||||
id: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
pauseStartTime?: number;
|
||||
pauseEndTime?: number;
|
||||
type: string;
|
||||
method: string;
|
||||
params: any;
|
||||
apiName?: string;
|
||||
stack?: StackFrame[];
|
||||
log: string[];
|
||||
snapshots: { title: string, snapshotName: string }[];
|
||||
error?: SerializedError;
|
||||
result?: any;
|
||||
point?: Point;
|
||||
objectId?: string;
|
||||
pageId?: string;
|
||||
frameId?: string;
|
||||
};
|
@ -15,8 +15,6 @@
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { Point, StackFrame } from '../common/types';
|
||||
import { SerializedError } from '../protocol/channels';
|
||||
import { createGuid } from '../utils/utils';
|
||||
import type { Browser } from './browser';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
@ -34,26 +32,8 @@ export type Attribution = {
|
||||
frame?: Frame;
|
||||
};
|
||||
|
||||
export type CallMetadata = {
|
||||
id: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
pauseStartTime?: number;
|
||||
pauseEndTime?: number;
|
||||
type: string;
|
||||
method: string;
|
||||
params: any;
|
||||
apiName?: string;
|
||||
stack?: StackFrame[];
|
||||
log: string[];
|
||||
snapshots: { title: string, snapshotName: string }[];
|
||||
error?: SerializedError;
|
||||
result?: any;
|
||||
point?: Point;
|
||||
objectId?: string;
|
||||
pageId?: string;
|
||||
frameId?: string;
|
||||
};
|
||||
import { CallMetadata } from '../protocol/callMetadata';
|
||||
export { CallMetadata } from '../protocol/callMetadata';
|
||||
|
||||
export class SdkObject extends EventEmitter {
|
||||
guid: string;
|
||||
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { StackFrame } from '../common/types';
|
||||
import { StackFrame } from '../protocol/channels';
|
||||
import StackUtils from 'stack-utils';
|
||||
import { isUnderTest } from './utils';
|
||||
|
||||
|
@ -16,7 +16,9 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import './callTab.css';
|
||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||
import type { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||
import { CallMetadata } from '../../../protocol/callMetadata';
|
||||
import { parseSerializedValue } from '../../../protocol/serializers';
|
||||
|
||||
export const CallTab: React.FunctionComponent<{
|
||||
action: ActionTraceEvent | undefined,
|
||||
@ -26,6 +28,7 @@ export const CallTab: React.FunctionComponent<{
|
||||
const logs = action.metadata.log;
|
||||
const error = action.metadata.error?.error?.message;
|
||||
const params = { ...action.metadata.params };
|
||||
// Strip down the waitForEventInfo data, we never need it.
|
||||
delete params.info;
|
||||
const paramKeys = Object.keys(params);
|
||||
return <div className='call-tab'>
|
||||
@ -36,14 +39,12 @@ export const CallTab: React.FunctionComponent<{
|
||||
<div className='call-line'>{action.metadata.apiName}</div>
|
||||
{ !!paramKeys.length && <div className='call-section'>Parameters</div> }
|
||||
{
|
||||
!!paramKeys.length && paramKeys.map(name =>
|
||||
<div className='call-line'>{name}: <span className={typeof params[name]}>{renderValue(params[name])}</span></div>
|
||||
)
|
||||
!!paramKeys.length && paramKeys.map(name => renderLine(action.metadata, name, params[name]))
|
||||
}
|
||||
{ !!action.metadata.result && <div className='call-section'>Return value</div> }
|
||||
{
|
||||
!!action.metadata.result && Object.keys(action.metadata.result).map(name =>
|
||||
<div className='call-line'>{name}: <span className={typeof action.metadata.result[name]}>{renderValue(action.metadata.result[name])}</span></div>
|
||||
renderLine(action.metadata, name, action.metadata.result[name])
|
||||
)
|
||||
}
|
||||
<div className='call-section'>Log</div>
|
||||
@ -57,10 +58,31 @@ export const CallTab: React.FunctionComponent<{
|
||||
</div>;
|
||||
};
|
||||
|
||||
function renderValue(value: any) {
|
||||
function renderLine(metadata: CallMetadata, name: string, value: any) {
|
||||
const { title, type } = toString(metadata, name, value);
|
||||
let text = trimRight(title.replace(/\n/g, '↵'), 80);
|
||||
if (type === 'string')
|
||||
text = `"${text}"`;
|
||||
return <div className='call-line'>{name}: <span className={type} title={title}>{text}</span></div>
|
||||
}
|
||||
|
||||
function toString(metadata: CallMetadata, name: string, value: any): { title: string, type: string } {
|
||||
if (metadata.method.includes('eval')) {
|
||||
if (name === 'arg')
|
||||
value = parseSerializedValue(value.value, new Array(10).fill({ handle: '<handle>' }));
|
||||
if (name === 'value')
|
||||
value = parseSerializedValue(value, new Array(10).fill({ handle: '<handle>' }));
|
||||
}
|
||||
const type = typeof value;
|
||||
if (type !== 'object')
|
||||
return String(value);
|
||||
return { title: String(value), type };
|
||||
if (value.guid)
|
||||
return '<handle>';
|
||||
return { title: '<handle>', type: 'handle' };
|
||||
return { title: JSON.stringify(value), type: 'object' };
|
||||
}
|
||||
|
||||
function trimRight(text: string, max: number): string {
|
||||
if (text.length > max)
|
||||
return text.substr(0, max) + '\u2026';
|
||||
return text;
|
||||
}
|
||||
|
@ -18,11 +18,11 @@ import * as React from 'react';
|
||||
import { useAsyncMemo } from './helpers';
|
||||
import './sourceTab.css';
|
||||
import '../../../third_party/highlightjs/highlightjs/tomorrow.css';
|
||||
import { StackFrame } from '../../../common/types';
|
||||
import { Source as SourceView } from '../../components/source';
|
||||
import { StackTraceView } from './stackTrace';
|
||||
import { SplitView } from '../../components/splitView';
|
||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||
import { StackFrame } from '../../../protocol/channels';
|
||||
|
||||
type StackInfo = string | {
|
||||
frames: StackFrame[];
|
||||
|
@ -42,7 +42,8 @@ class TraceViewerPage {
|
||||
await this.page.click(`.action-title:has-text("${title}")`);
|
||||
}
|
||||
|
||||
async logLines() {
|
||||
|
||||
async callLines() {
|
||||
await this.page.waitForSelector('.call-line:visible');
|
||||
return await this.page.$$eval('.call-line:visible', ee => ee.map(e => e.textContent));
|
||||
}
|
||||
@ -97,12 +98,13 @@ test.beforeAll(async ({ browser, browserName }, workerInfo) => {
|
||||
const page = await context.newPage();
|
||||
await page.goto('data:text/html,<html>Hello world</html>');
|
||||
await page.setContent('<button>Click</button>');
|
||||
await page.evaluate(() => {
|
||||
await page.evaluate(({ a }) => {
|
||||
console.log('Info');
|
||||
console.warn('Warning');
|
||||
console.error('Error');
|
||||
setTimeout(() => { throw new Error('Unhandled exception'); }, 0);
|
||||
});
|
||||
return 'return ' + a;
|
||||
}, { a: 'paramA', b: 4 });
|
||||
await page.click('"Click"');
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
@ -133,7 +135,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||
test('should contain action info', async ({ showTraceViewer }) => {
|
||||
const traceViewer = await showTraceViewer(traceFile);
|
||||
await traceViewer.selectAction('page.click');
|
||||
const logLines = await traceViewer.logLines();
|
||||
const logLines = await traceViewer.callLines();
|
||||
expect(logLines.length).toBeGreaterThan(10);
|
||||
expect(logLines).toContain('attempting click action');
|
||||
expect(logLines).toContain(' click action done');
|
||||
@ -168,3 +170,15 @@ test('should open console errors on click', async ({ showTraceViewer, browserNam
|
||||
await (await traceViewer.actionIcons('page.evaluate')).click();
|
||||
expect(await traceViewer.page.waitForSelector('.console-tab'));
|
||||
});
|
||||
|
||||
test('should show params and return value', async ({ showTraceViewer, browserName }) => {
|
||||
const traceViewer = await showTraceViewer(traceFile);
|
||||
expect(await traceViewer.selectAction('page.evaluate'));
|
||||
expect(await traceViewer.callLines()).toEqual([
|
||||
'page.evaluate',
|
||||
'expression: "({↵ a↵ }) => {↵ console.log(\'Info\');↵ console.warn(\'Warning\');↵ con…"',
|
||||
'isFunction: true',
|
||||
'arg: {"a":"paramA","b":4}',
|
||||
'value: "return paramA"'
|
||||
]);
|
||||
});
|
||||
|
@ -147,7 +147,7 @@ DEPS['src/cli/driver.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerIm
|
||||
// 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/'];
|
||||
DEPS['src/web/traceViewer/ui/'] = ['src/common/', 'src/web/traceViewer/', 'src/web/', 'src/server/trace/viewer/', 'src/server/trace/', 'src/server/trace/common/', 'src/server/snapshot/snapshotTypes.ts', 'src/protocol/channels.ts'];
|
||||
DEPS['src/web/traceViewer/ui/'] = ['src/common/', 'src/protocol/', 'src/web/traceViewer/', 'src/web/', 'src/server/trace/viewer/', 'src/server/trace/', 'src/server/trace/common/', 'src/server/snapshot/snapshotTypes.ts', 'src/protocol/channels.ts'];
|
||||
// The service is a cross-cutting feature, and so it depends on a bunch of things.
|
||||
DEPS['src/remote/'] = ['src/client/', 'src/debug/', 'src/dispatchers/', 'src/server/', 'src/server/supplements/', 'src/server/electron/', 'src/server/trace/'];
|
||||
|
||||
@ -156,7 +156,7 @@ DEPS['src/cli/'] = ['src/cli/**', 'src/client/**', 'src/install/**', 'src/genera
|
||||
|
||||
DEPS['src/server/supplements/recorder/recorderApp.ts'] = ['src/common/', 'src/utils/', 'src/server/', 'src/server/chromium/'];
|
||||
DEPS['src/server/supplements/recorderSupplement.ts'] = ['src/server/snapshot/', ...DEPS['src/server/']];
|
||||
DEPS['src/utils/'] = ['src/common/'];
|
||||
DEPS['src/utils/'] = ['src/common/', 'src/protocol/'];
|
||||
|
||||
// Trace viewer
|
||||
DEPS['src/server/trace/common/'] = ['src/server/snapshot/', ...DEPS['src/server/']];
|
||||
|
Loading…
Reference in New Issue
Block a user