feat: Add time information to Call and Network tabs in Trace Viewer (#33935)
This commit is contained in:
parent
cc98166aaa
commit
05472f5ef6
|
|
@ -27,50 +27,63 @@ import type { ActionTraceEventInContext } from './modelUtil';
|
||||||
|
|
||||||
export const CallTab: React.FunctionComponent<{
|
export const CallTab: React.FunctionComponent<{
|
||||||
action: ActionTraceEventInContext | undefined,
|
action: ActionTraceEventInContext | undefined,
|
||||||
|
startTimeOffset: number,
|
||||||
sdkLanguage: Language | undefined,
|
sdkLanguage: Language | undefined,
|
||||||
}> = ({ action, sdkLanguage }) => {
|
}> = ({ action, startTimeOffset, sdkLanguage }) => {
|
||||||
|
// We never need the waitForEventInfo (`info`).
|
||||||
|
const paramKeys = React.useMemo(() => Object.keys(action?.params ?? {}).filter(name => name !== 'info'), [action]);
|
||||||
|
|
||||||
if (!action)
|
if (!action)
|
||||||
return <PlaceholderPanel text='No action selected' />;
|
return <PlaceholderPanel text='No action selected' />;
|
||||||
const params = { ...action.params };
|
|
||||||
// Strip down the waitForEventInfo data, we never need it.
|
// Calculate execution time relative to the test runner's start time
|
||||||
delete params.info;
|
const startTimeMillis = action.startTime - startTimeOffset;
|
||||||
const paramKeys = Object.keys(params);
|
const startTime = msToString(startTimeMillis);
|
||||||
const timeMillis = action.startTime + (action.context.wallTime - action.context.startTime);
|
|
||||||
const wallTime = new Date(timeMillis).toLocaleString();
|
|
||||||
const duration = action.endTime ? msToString(action.endTime - action.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-line'>{action.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>}
|
<DateTimeCallLine name='start:' value={startTime} />
|
||||||
<div className='call-line'>duration:<span className='call-value datetime' title={duration}>{duration}</span></div>
|
<DateTimeCallLine name='duration:' value={duration} />
|
||||||
</>}
|
</>
|
||||||
{ !!paramKeys.length && <div className='call-section'>Parameters</div> }
|
|
||||||
{
|
|
||||||
!!paramKeys.length && paramKeys.map((name, index) => renderProperty(propertyToString(action, name, params[name], sdkLanguage), 'param-' + index))
|
|
||||||
}
|
}
|
||||||
{ !!action.result && <div className='call-section'>Return value</div> }
|
|
||||||
{
|
{
|
||||||
!!action.result && Object.keys(action.result).map((name, index) =>
|
!!paramKeys.length && <>
|
||||||
renderProperty(propertyToString(action, name, action.result[name], sdkLanguage), 'result-' + index)
|
<div className='call-section'>Parameters</div>
|
||||||
)
|
{paramKeys.map(name => renderProperty(propertyToString(action, name, action.params[name], sdkLanguage)))}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
</div>;
|
{
|
||||||
|
!!action.result && <>
|
||||||
|
<div className='call-section'>Return value</div>
|
||||||
|
{Object.keys(action.result).map(name =>
|
||||||
|
renderProperty(propertyToString(action, name, action.result[name], sdkLanguage))
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DateTimeCallLine: React.FC<{ name: string, value: string }> = ({ name, value }) => <div className='call-line'>{name}<span className='call-value datetime' title={value}>{value}</span></div>;
|
||||||
|
|
||||||
type Property = {
|
type Property = {
|
||||||
name: string;
|
name: string;
|
||||||
type: 'string' | 'number' | 'object' | 'locator' | 'handle' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'function';
|
type: 'string' | 'number' | 'object' | 'locator' | 'handle' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'function';
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderProperty(property: Property, key: string) {
|
function renderProperty(property: Property) {
|
||||||
let text = property.text.replace(/\n/g, '↵');
|
let text = property.text.replace(/\n/g, '↵');
|
||||||
if (property.type === 'string')
|
if (property.type === 'string')
|
||||||
text = `"${text}"`;
|
text = `"${text}"`;
|
||||||
return (
|
return (
|
||||||
<div key={key} className='call-line'>
|
<div key={property.name} className='call-line'>
|
||||||
{property.name}:<span className={clsx('call-value', property.type)} title={property.text}>{text}</span>
|
{property.name}:<span className={clsx('call-value', property.type)} title={property.text}>{text}</span>
|
||||||
{ ['string', 'number', 'object', 'locator'].includes(property.type) &&
|
{ ['string', 'number', 'object', 'locator'].includes(property.type) &&
|
||||||
<CopyToClipboard value={property.text} />
|
<CopyToClipboard value={property.text} />
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,11 @@ export const MetadataView: React.FunctionComponent<{
|
||||||
if (!model)
|
if (!model)
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|
||||||
|
const wallTime = model.wallTime !== undefined ? new Date(model.wallTime).toLocaleString(undefined, { timeZoneName: 'short' }) : undefined;
|
||||||
|
|
||||||
return <div data-testid='metadata-view' className='vbox' style={{ flexShrink: 0 }}>
|
return <div data-testid='metadata-view' className='vbox' style={{ flexShrink: 0 }}>
|
||||||
<div className='call-section' style={{ paddingTop: 2 }}>Time</div>
|
<div className='call-section' style={{ paddingTop: 2 }}>Time</div>
|
||||||
{!!model.wallTime && <div className='call-line'>start time:<span className='call-value datetime' title={new Date(model.wallTime).toLocaleString()}>{new Date(model.wallTime).toLocaleString()}</span></div>}
|
{!!wallTime && <div className='call-line'>start time:<span className='call-value datetime' title={wallTime}>{wallTime}</span></div>}
|
||||||
<div className='call-line'>duration:<span className='call-value number' title={msToString(model.endTime - model.startTime)}>{msToString(model.endTime - model.startTime)}</span></div>
|
<div className='call-line'>duration:<span className='call-value number' title={msToString(model.endTime - model.startTime)}>{msToString(model.endTime - model.startTime)}</span></div>
|
||||||
<div className='call-section'>Browser</div>
|
<div className='call-section'>Browser</div>
|
||||||
<div className='call-line'>engine:<span className='call-value string' title={model.browserName}>{model.browserName}</span></div>
|
<div className='call-line'>engine:<span className='call-value string' title={model.browserName}>{model.browserName}</span></div>
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,14 @@ import { generateCurlCommand, generateFetchCall } from '../third_party/devtools'
|
||||||
import { CopyToClipboardTextButton } from './copyToClipboard';
|
import { CopyToClipboardTextButton } from './copyToClipboard';
|
||||||
import { getAPIRequestCodeGen } from './codegen';
|
import { getAPIRequestCodeGen } from './codegen';
|
||||||
import type { Language } from '@isomorphic/locatorGenerators';
|
import type { Language } from '@isomorphic/locatorGenerators';
|
||||||
|
import { msToString } from '@web/uiUtils';
|
||||||
|
|
||||||
export const NetworkResourceDetails: React.FunctionComponent<{
|
export const NetworkResourceDetails: React.FunctionComponent<{
|
||||||
resource: ResourceSnapshot;
|
resource: ResourceSnapshot;
|
||||||
onClose: () => void;
|
|
||||||
sdkLanguage: Language;
|
sdkLanguage: Language;
|
||||||
}> = ({ resource, onClose, sdkLanguage }) => {
|
startTimeOffset: number;
|
||||||
|
onClose: () => void;
|
||||||
|
}> = ({ resource, sdkLanguage, startTimeOffset, onClose }) => {
|
||||||
const [selectedTab, setSelectedTab] = React.useState('request');
|
const [selectedTab, setSelectedTab] = React.useState('request');
|
||||||
|
|
||||||
return <TabbedPane
|
return <TabbedPane
|
||||||
|
|
@ -39,7 +41,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{
|
||||||
{
|
{
|
||||||
id: 'request',
|
id: 'request',
|
||||||
title: 'Request',
|
title: 'Request',
|
||||||
render: () => <RequestTab resource={resource} sdkLanguage={sdkLanguage} />,
|
render: () => <RequestTab resource={resource} sdkLanguage={sdkLanguage} startTimeOffset={startTimeOffset} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'response',
|
id: 'response',
|
||||||
|
|
@ -59,7 +61,8 @@ export const NetworkResourceDetails: React.FunctionComponent<{
|
||||||
const RequestTab: React.FunctionComponent<{
|
const RequestTab: React.FunctionComponent<{
|
||||||
resource: ResourceSnapshot;
|
resource: ResourceSnapshot;
|
||||||
sdkLanguage: Language;
|
sdkLanguage: Language;
|
||||||
}> = ({ resource, sdkLanguage }) => {
|
startTimeOffset: number;
|
||||||
|
}> = ({ resource, sdkLanguage, startTimeOffset }) => {
|
||||||
const [requestBody, setRequestBody] = React.useState<{ text: string, mimeType?: string } | null>(null);
|
const [requestBody, setRequestBody] = React.useState<{ text: string, mimeType?: string } | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -96,6 +99,9 @@ const RequestTab: React.FunctionComponent<{
|
||||||
</> : null}
|
</> : null}
|
||||||
<div className='network-request-details-header'>Request Headers</div>
|
<div className='network-request-details-header'>Request Headers</div>
|
||||||
<div className='network-request-details-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
<div className='network-request-details-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
||||||
|
<div className='network-request-details-header'>Time</div>
|
||||||
|
<div className='network-request-details-general'>{`Start: ${msToString(startTimeOffset)}`}</div>
|
||||||
|
<div className='network-request-details-general'>{`Duration: ${msToString(resource.time)}`}</div>
|
||||||
|
|
||||||
<div className='network-request-details-copy'>
|
<div className='network-request-details-copy'>
|
||||||
<CopyToClipboardTextButton description='Copy as cURL' value={() => generateCurlCommand(resource)} />
|
<CopyToClipboardTextButton description='Copy as cURL' value={() => generateCurlCommand(resource)} />
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ export const NetworkTab: React.FunctionComponent<{
|
||||||
sidebarIsFirst={true}
|
sidebarIsFirst={true}
|
||||||
orientation='horizontal'
|
orientation='horizontal'
|
||||||
settingName='networkResourceDetails'
|
settingName='networkResourceDetails'
|
||||||
main={<NetworkResourceDetails resource={selectedEntry.resource} onClose={() => setSelectedEntry(undefined)} sdkLanguage={sdkLanguage} />}
|
main={<NetworkResourceDetails resource={selectedEntry.resource} sdkLanguage={sdkLanguage} startTimeOffset={selectedEntry.start} onClose={() => setSelectedEntry(undefined)} />}
|
||||||
sidebar={grid}
|
sidebar={grid}
|
||||||
/>}
|
/>}
|
||||||
</>;
|
</>;
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
const callTab: TabbedPaneTabModel = {
|
const callTab: TabbedPaneTabModel = {
|
||||||
id: 'call',
|
id: 'call',
|
||||||
title: 'Call',
|
title: 'Call',
|
||||||
render: () => <CallTab action={activeAction} sdkLanguage={sdkLanguage} />
|
render: () => <CallTab action={activeAction} startTimeOffset={model?.startTime ?? 0} sdkLanguage={sdkLanguage} />
|
||||||
};
|
};
|
||||||
const logTab: TabbedPaneTabModel = {
|
const logTab: TabbedPaneTabModel = {
|
||||||
id: 'log',
|
id: 'log',
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,7 @@ test('should show params and return value', async ({ showTraceViewer }) => {
|
||||||
await traceViewer.selectAction('page.evaluate');
|
await traceViewer.selectAction('page.evaluate');
|
||||||
await expect(traceViewer.callLines).toHaveText([
|
await expect(traceViewer.callLines).toHaveText([
|
||||||
/page.evaluate/,
|
/page.evaluate/,
|
||||||
/wall time:[0-9/:,APM ]+/,
|
/start:[\d\.]+m?s/,
|
||||||
/duration:[\d]+ms/,
|
/duration:[\d]+ms/,
|
||||||
/expression:"\({↵ a↵ }\) => {↵ console\.log\(\'Info\'\);↵ console\.warn\(\'Warning\'\);↵ console/,
|
/expression:"\({↵ a↵ }\) => {↵ console\.log\(\'Info\'\);↵ console\.warn\(\'Warning\'\);↵ console/,
|
||||||
'isFunction:true',
|
'isFunction:true',
|
||||||
|
|
@ -251,7 +251,7 @@ test('should show params and return value', async ({ showTraceViewer }) => {
|
||||||
await traceViewer.selectAction(`locator('button')`);
|
await traceViewer.selectAction(`locator('button')`);
|
||||||
await expect(traceViewer.callLines).toContainText([
|
await expect(traceViewer.callLines).toContainText([
|
||||||
/expect.toHaveText/,
|
/expect.toHaveText/,
|
||||||
/wall time:[0-9/:,APM ]+/,
|
/start:[\d\.]+m?s/,
|
||||||
/duration:[\d]+ms/,
|
/duration:[\d]+ms/,
|
||||||
/locator:locator\('button'\)/,
|
/locator:locator\('button'\)/,
|
||||||
/expression:"to.have.text"/,
|
/expression:"to.have.text"/,
|
||||||
|
|
@ -266,7 +266,7 @@ test('should show null as a param', async ({ showTraceViewer, browserName }) =>
|
||||||
await traceViewer.selectAction('page.evaluate', 1);
|
await traceViewer.selectAction('page.evaluate', 1);
|
||||||
await expect(traceViewer.callLines).toHaveText([
|
await expect(traceViewer.callLines).toHaveText([
|
||||||
/page.evaluate/,
|
/page.evaluate/,
|
||||||
/wall time:[0-9/:,APM ]+/,
|
/start:[\d\.]+m?s/,
|
||||||
/duration:[\d]+ms/,
|
/duration:[\d]+ms/,
|
||||||
'expression:"() => 1 + 1"',
|
'expression:"() => 1 + 1"',
|
||||||
'isFunction:true',
|
'isFunction:true',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue