cherry-pick(#20937): chore: minor trace viewer UI tweaks
This commit is contained in:
parent
24be5c2881
commit
8d3481ea22
|
|
@ -15,11 +15,10 @@
|
|||
*/
|
||||
|
||||
import '@web/third_party/vscode/codicon.css';
|
||||
import { Workbench } from './ui/workbench';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { applyTheme } from '@web/theme';
|
||||
import '@web/common.css';
|
||||
import { WorkbenchLoader } from './ui/workbench';
|
||||
|
||||
(async () => {
|
||||
applyTheme();
|
||||
|
|
@ -37,5 +36,5 @@ import '@web/common.css';
|
|||
setInterval(function() { fetch('ping'); }, 10000);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Workbench/>, document.querySelector('#root'));
|
||||
ReactDOM.render(<WorkbenchLoader></WorkbenchLoader>, document.querySelector('#root'));
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -14,50 +14,6 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.action-list {
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.action-list-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
overflow: auto;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.action-entry {
|
||||
display: flex;
|
||||
flex: none;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
line-height: 28px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.action-entry.highlighted,
|
||||
.action-entry.selected {
|
||||
background-color: var(--vscode-list-inactiveSelectionBackground);
|
||||
}
|
||||
|
||||
.action-entry.highlighted {
|
||||
background-color: var(--vscode-list-inactiveSelectionBackground);
|
||||
}
|
||||
|
||||
.action-list-content:focus .action-entry.selected {
|
||||
background-color: var(--vscode-list-activeSelectionBackground);
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.action-list-content:focus .action-entry.selected * {
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
}
|
||||
|
||||
.action-title {
|
||||
flex: auto;
|
||||
display: block;
|
||||
|
|
@ -124,10 +80,3 @@
|
|||
.action-entry .codicon-warning {
|
||||
color: darkorange;
|
||||
}
|
||||
|
||||
.no-actions-entry {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
import type { ActionTraceEvent } from '@trace/trace';
|
||||
import { msToString } from '@web/uiUtils';
|
||||
import { ListView } from '@web/components/listView';
|
||||
import * as React from 'react';
|
||||
import './actionList.css';
|
||||
import * as modelUtil from './modelUtil';
|
||||
|
|
@ -26,7 +27,6 @@ import type { Language } from '@isomorphic/locatorGenerators';
|
|||
export interface ActionListProps {
|
||||
actions: ActionTraceEvent[],
|
||||
selectedAction: ActionTraceEvent | undefined,
|
||||
highlightedAction: ActionTraceEvent | undefined,
|
||||
sdkLanguage: Language | undefined;
|
||||
onSelected: (action: ActionTraceEvent) => void,
|
||||
onHighlighted: (action: ActionTraceEvent | undefined) => void,
|
||||
|
|
@ -36,92 +36,33 @@ export interface ActionListProps {
|
|||
export const ActionList: React.FC<ActionListProps> = ({
|
||||
actions = [],
|
||||
selectedAction,
|
||||
highlightedAction,
|
||||
sdkLanguage,
|
||||
onSelected = () => {},
|
||||
onHighlighted = () => {},
|
||||
setSelectedTab = () => {},
|
||||
}) => {
|
||||
const actionListRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
React.useEffect(() => {
|
||||
actionListRef.current?.focus();
|
||||
}, [selectedAction, actionListRef]);
|
||||
|
||||
return <div className='action-list vbox'>
|
||||
<div
|
||||
className='action-list-content'
|
||||
tabIndex={0}
|
||||
onKeyDown={event => {
|
||||
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
|
||||
return;
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const index = selectedAction ? actions.indexOf(selectedAction) : -1;
|
||||
let newIndex = index;
|
||||
if (event.key === 'ArrowDown') {
|
||||
if (index === -1)
|
||||
newIndex = 0;
|
||||
else
|
||||
newIndex = Math.min(index + 1, actions.length - 1);
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
if (index === -1)
|
||||
newIndex = actions.length - 1;
|
||||
else
|
||||
newIndex = Math.max(index - 1, 0);
|
||||
}
|
||||
const element = actionListRef.current?.children.item(newIndex);
|
||||
scrollIntoViewIfNeeded(element);
|
||||
onSelected(actions[newIndex]);
|
||||
}}
|
||||
ref={actionListRef}
|
||||
>
|
||||
{actions.length === 0 && <div className='no-actions-entry'>No actions recorded</div>}
|
||||
{actions.map(action => <ActionListItem
|
||||
action={action}
|
||||
highlightedAction={highlightedAction}
|
||||
onSelected={onSelected}
|
||||
onHighlighted={onHighlighted}
|
||||
selectedAction={selectedAction}
|
||||
sdkLanguage={sdkLanguage}
|
||||
setSelectedTab={setSelectedTab}
|
||||
/>)}
|
||||
</div>
|
||||
</div>;
|
||||
return <ListView
|
||||
items={actions}
|
||||
selectedItem={selectedAction}
|
||||
onSelected={(action: ActionTraceEvent) => onSelected(action)}
|
||||
onHighlighted={(action: ActionTraceEvent) => onHighlighted(action)}
|
||||
itemKey={(action: ActionTraceEvent) => action.metadata.id}
|
||||
itemRender={(action: ActionTraceEvent) => renderAction(action, sdkLanguage, setSelectedTab)}
|
||||
showNoItemsMessage={true}
|
||||
></ListView>;
|
||||
};
|
||||
|
||||
const ActionListItem: React.FC<{
|
||||
const renderAction = (
|
||||
action: ActionTraceEvent,
|
||||
highlightedAction: ActionTraceEvent | undefined,
|
||||
onSelected: (action: ActionTraceEvent) => void,
|
||||
onHighlighted: (action: ActionTraceEvent | undefined) => void,
|
||||
selectedAction: ActionTraceEvent | undefined,
|
||||
sdkLanguage: Language | undefined,
|
||||
setSelectedTab: (tab: string) => void,
|
||||
}> = ({ action, onSelected, onHighlighted, highlightedAction, selectedAction, sdkLanguage, setSelectedTab }) => {
|
||||
setSelectedTab: (tab: string) => void
|
||||
) => {
|
||||
const { metadata } = action;
|
||||
const selectedSuffix = action === selectedAction ? ' selected' : '';
|
||||
const highlightedSuffix = action === highlightedAction ? ' highlighted' : '';
|
||||
const error = metadata.error?.error?.message;
|
||||
const { errors, warnings } = modelUtil.stats(action);
|
||||
const locator = metadata.params.selector ? asLocator(sdkLanguage || 'javascript', metadata.params.selector) : undefined;
|
||||
|
||||
const divRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (divRef.current && selectedAction === action)
|
||||
scrollIntoViewIfNeeded(divRef.current);
|
||||
}, [selectedAction, action]);
|
||||
|
||||
return <div
|
||||
className={'action-entry' + selectedSuffix + highlightedSuffix}
|
||||
key={metadata.id}
|
||||
onClick={() => onSelected(action)}
|
||||
onMouseEnter={() => onHighlighted(action)}
|
||||
onMouseLeave={() => (highlightedAction === action) && onHighlighted(undefined)}
|
||||
ref={divRef}
|
||||
>
|
||||
return <>
|
||||
<div className='action-title'>
|
||||
<span>{metadata.apiName}</span>
|
||||
{locator && <div className='action-selector' title={locator}>{locator}</div>}
|
||||
|
|
@ -133,14 +74,5 @@ const ActionListItem: React.FC<{
|
|||
{!!warnings && <div className='action-icon'><span className={'codicon codicon-warning'}></span><span className="action-icon-value">{warnings}</span></div>}
|
||||
</div>
|
||||
{error && <div className='codicon codicon-issues' title={error} />}
|
||||
</div>;
|
||||
</>;
|
||||
};
|
||||
|
||||
function scrollIntoViewIfNeeded(element?: Element | null) {
|
||||
if (!element)
|
||||
return;
|
||||
if ((element as any)?.scrollIntoViewIfNeeded)
|
||||
(element as any).scrollIntoViewIfNeeded(false);
|
||||
else
|
||||
element?.scrollIntoView();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,10 +41,8 @@
|
|||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
color: var(--vscode-sideBarTitle-foreground);
|
||||
line-height: 18px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.call-line {
|
||||
|
|
@ -68,6 +66,7 @@
|
|||
}
|
||||
|
||||
.call-value {
|
||||
margin-left: 2px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
|
|
@ -77,18 +76,18 @@
|
|||
content: '\00a0';
|
||||
}
|
||||
|
||||
.call-line .datetime,
|
||||
.call-line .string,
|
||||
.call-line .locator {
|
||||
.call-value.datetime,
|
||||
.call-value.string,
|
||||
.call-value.locator {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.call-line .number,
|
||||
.call-line .bigint,
|
||||
.call-line .boolean,
|
||||
.call-line .symbol,
|
||||
.call-line .undefined,
|
||||
.call-line .function,
|
||||
.call-line .object {
|
||||
.call-value.number,
|
||||
.call-value.bigint,
|
||||
.call-value.boolean,
|
||||
.call-value.symbol,
|
||||
.call-value.undefined,
|
||||
.call-value.function,
|
||||
.call-value.object {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ export const CallTab: React.FunctionComponent<{
|
|||
<div className='call-line'>{action.metadata.apiName}</div>
|
||||
{<>
|
||||
<div className='call-section'>Time</div>
|
||||
{action.metadata.wallTime && <div className='call-line'>wall time: <span className='call-value datetime' title={wallTime}>{wallTime}</span></div>}
|
||||
<div className='call-line'>duration: <span className='call-value datetime' title={duration}>{duration}</span></div>
|
||||
{action.metadata.wallTime && <div className='call-line'>wall time:<span className='call-value datetime' title={wallTime}>{wallTime}</span></div>}
|
||||
<div className='call-line'>duration:<span className='call-value datetime' title={duration}>{duration}</span></div>
|
||||
</>}
|
||||
{ !!paramKeys.length && <div className='call-section'>Parameters</div> }
|
||||
{
|
||||
|
|
@ -82,7 +82,7 @@ function renderProperty(property: Property, key: string) {
|
|||
text = `"${text}"`;
|
||||
return (
|
||||
<div key={key} className='call-line'>
|
||||
{property.name}: <span className={`call-value ${property.type}`} title={property.text}>{text}</span>
|
||||
{property.name}:<span className={`call-value ${property.type}`} title={property.text}>{text}</span>
|
||||
{ ['string', 'number', 'object', 'locator'].includes(property.type) &&
|
||||
<CopyToClipboard value={property.text} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.codicon-check {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.codicon-close {
|
||||
color: var(--red);
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import './copyToClipboard.css';
|
||||
|
||||
export const CopyToClipboard: React.FunctionComponent<{
|
||||
value: string,
|
||||
|
|
|
|||
|
|
@ -15,11 +15,10 @@
|
|||
*/
|
||||
|
||||
.network-request {
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
padding: 0 3px;
|
||||
width: 100%;
|
||||
flex: none;
|
||||
outline: none;
|
||||
}
|
||||
|
|
@ -58,6 +57,7 @@
|
|||
.network-request-details {
|
||||
width: 100%;
|
||||
user-select: text;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.network-request-details-url {
|
||||
|
|
@ -84,6 +84,7 @@
|
|||
background-color: var(--vscode-sideBar-background);
|
||||
border: black 1px solid;
|
||||
max-height: 500px;
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.network-request-details-header {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
align-items: stretch;
|
||||
outline: none;
|
||||
--window-header-height: 40px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.snapshot-controls {
|
||||
|
|
@ -85,18 +86,25 @@ iframe#snapshot {
|
|||
|
||||
.popout-icon {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
color: var(--gray);
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: var(--vscode-sideBarTitle-foreground);
|
||||
font-size: 14px;
|
||||
z-index: 100;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.popout-icon:not(.popout-disabled):hover {
|
||||
color: var(--blue);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.popout-icon.popout-disabled {
|
||||
opacity: 0.7;
|
||||
opacity: var(--vscode-disabledForeground);
|
||||
}
|
||||
|
||||
.window-dot {
|
||||
|
|
@ -109,11 +117,11 @@ iframe#snapshot {
|
|||
}
|
||||
|
||||
.window-address-bar {
|
||||
background-color: white;
|
||||
background-color: var(--vscode-input-background);
|
||||
border-radius: 12.5px;
|
||||
color: #1c1e21;
|
||||
color: var(--vscode-input-foreground);
|
||||
flex: 1 0;
|
||||
font: 400 13px Arial,sans-serif;
|
||||
font: 400 16px Arial,sans-serif;
|
||||
margin: 0 16px 0 8px;
|
||||
padding: 5px 15px;
|
||||
overflow: hidden;
|
||||
|
|
@ -121,11 +129,6 @@ iframe#snapshot {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
body.dark-mode .window-address-bar {
|
||||
background-color: #1b1b1d;
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
.window-menu-bar {
|
||||
background-color: #aaa;
|
||||
display: block;
|
||||
|
|
|
|||
|
|
@ -41,10 +41,8 @@ export const Timeline: React.FunctionComponent<{
|
|||
context: MultiTraceModel,
|
||||
boundaries: Boundaries,
|
||||
selectedAction: ActionTraceEvent | undefined,
|
||||
highlightedAction: ActionTraceEvent | undefined,
|
||||
onSelected: (action: ActionTraceEvent) => void,
|
||||
onHighlighted: (action: ActionTraceEvent | undefined) => void,
|
||||
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onHighlighted }) => {
|
||||
}> = ({ context, boundaries, selectedAction, onSelected }) => {
|
||||
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||
const barsRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
|
|
@ -92,7 +90,7 @@ export const Timeline: React.FunctionComponent<{
|
|||
}, [context, boundaries, measure.width]);
|
||||
|
||||
const hoveredBar = hoveredBarIndex !== undefined ? bars[hoveredBarIndex] : undefined;
|
||||
let targetBar: TimelineBar | undefined = bars.find(bar => bar.action === (highlightedAction || selectedAction));
|
||||
let targetBar: TimelineBar | undefined = bars.find(bar => bar.action === selectedAction);
|
||||
targetBar = hoveredBar || targetBar;
|
||||
|
||||
const findHoveredBarIndex = (x: number, y: number) => {
|
||||
|
|
@ -132,14 +130,11 @@ export const Timeline: React.FunctionComponent<{
|
|||
const index = findHoveredBarIndex(x, y);
|
||||
setPreviewPoint({ x, clientY: event.clientY });
|
||||
setHoveredBarIndex(index);
|
||||
if (typeof index === 'number')
|
||||
onHighlighted(bars[index].action);
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
setPreviewPoint(undefined);
|
||||
setHoveredBarIndex(undefined);
|
||||
onHighlighted(undefined);
|
||||
};
|
||||
|
||||
const onClick = (event: React.MouseEvent) => {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@
|
|||
contain: size;
|
||||
}
|
||||
|
||||
.workbench .header {
|
||||
.header {
|
||||
display: flex;
|
||||
background-color: #000;
|
||||
flex: none;
|
||||
|
|
|
|||
|
|
@ -33,15 +33,11 @@ import { Timeline } from './timeline';
|
|||
import './workbench.css';
|
||||
import { toggleTheme } from '@web/theme';
|
||||
|
||||
export const Workbench: React.FunctionComponent<{
|
||||
export const WorkbenchLoader: React.FunctionComponent<{
|
||||
}> = () => {
|
||||
const [traceURLs, setTraceURLs] = React.useState<string[]>([]);
|
||||
const [uploadedTraceNames, setUploadedTraceNames] = React.useState<string[]>([]);
|
||||
const [model, setModel] = React.useState<MultiTraceModel>(emptyModel);
|
||||
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
||||
const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState<string>('logs');
|
||||
const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 });
|
||||
const [dragOver, setDragOver] = React.useState<boolean>(false);
|
||||
const [processingErrorMessage, setProcessingErrorMessage] = React.useState<string | null>(null);
|
||||
|
|
@ -67,7 +63,6 @@ export const Workbench: React.FunctionComponent<{
|
|||
window.history.pushState({}, '', href);
|
||||
setTraceURLs(blobUrls);
|
||||
setUploadedTraceNames(fileNames);
|
||||
setSelectedAction(undefined);
|
||||
setDragOver(false);
|
||||
setProcessingErrorMessage(null);
|
||||
};
|
||||
|
|
@ -134,24 +129,6 @@ export const Workbench: React.FunctionComponent<{
|
|||
})();
|
||||
}, [traceURLs, uploadedTraceNames]);
|
||||
|
||||
const boundaries = { minimum: model.startTime, maximum: model.endTime };
|
||||
|
||||
|
||||
// Leave some nice free space on the right hand side.
|
||||
boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20;
|
||||
const { errors, warnings } = selectedAction ? modelUtil.stats(selectedAction) : { errors: 0, warnings: 0 };
|
||||
const consoleCount = errors + warnings;
|
||||
const networkCount = selectedAction ? modelUtil.resourcesForAction(selectedAction).length : 0;
|
||||
|
||||
const tabs = [
|
||||
{ id: 'logs', title: 'Call', count: 0, render: () => <CallTab action={selectedAction} sdkLanguage={model.sdkLanguage} /> },
|
||||
{ id: 'console', title: 'Console', count: consoleCount, render: () => <ConsoleTab action={selectedAction} /> },
|
||||
{ id: 'network', title: 'Network', count: networkCount, render: () => <NetworkTab action={selectedAction} /> },
|
||||
];
|
||||
|
||||
if (model.hasSource)
|
||||
tabs.push({ id: 'source', title: 'Source', count: 0, render: () => <SourceTab action={selectedAction} /> });
|
||||
|
||||
return <div className='vbox workbench' onDragOver={event => { event.preventDefault(); setDragOver(true); }}>
|
||||
<div className='hbox header'>
|
||||
<div className='logo'>🎭</div>
|
||||
|
|
@ -160,55 +137,7 @@ export const Workbench: React.FunctionComponent<{
|
|||
<div className='spacer'></div>
|
||||
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
|
||||
</div>
|
||||
<div style={{ paddingLeft: '20px', flex: 'none', borderBottom: '1px solid var(--vscode-panel-border)' }}>
|
||||
<Timeline
|
||||
context={model}
|
||||
boundaries={boundaries}
|
||||
selectedAction={selectedAction}
|
||||
highlightedAction={highlightedAction}
|
||||
onSelected={action => setSelectedAction(action)}
|
||||
onHighlighted={action => setHighlightedAction(action)}
|
||||
/>
|
||||
</div>
|
||||
<SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
|
||||
<SplitView sidebarSize={300} orientation='horizontal'>
|
||||
<SnapshotTab action={selectedAction} />
|
||||
<TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab}/>
|
||||
</SplitView>
|
||||
<TabbedPane tabs={
|
||||
[
|
||||
{ id: 'actions', title: 'Actions', count: 0, render: () => <ActionList
|
||||
sdkLanguage={model.sdkLanguage}
|
||||
actions={model.actions}
|
||||
selectedAction={selectedAction}
|
||||
highlightedAction={highlightedAction}
|
||||
onSelected={action => {
|
||||
setSelectedAction(action);
|
||||
}}
|
||||
onHighlighted={action => setHighlightedAction(action)}
|
||||
setSelectedTab={setSelectedPropertiesTab}
|
||||
/> },
|
||||
{ id: 'metadata', title: 'Metadata', count: 0, render: () => <div className='vbox'>
|
||||
<div className='call-section' style={{ paddingTop: 2 }}>Time</div>
|
||||
{model.wallTime && <div className='call-line'>start time: <span className='datetime' title={new Date(model.wallTime).toLocaleString()}>{new Date(model.wallTime).toLocaleString()}</span></div>}
|
||||
<div className='call-line'>duration: <span className='number' title={msToString(model.endTime - model.startTime)}>{msToString(model.endTime - model.startTime)}</span></div>
|
||||
<div className='call-section'>Browser</div>
|
||||
<div className='call-line'>engine: <span className='string' title={model.browserName}>{model.browserName}</span></div>
|
||||
{model.platform && <div className='call-line'>platform: <span className='string' title={model.platform}>{model.platform}</span></div>}
|
||||
{model.options.userAgent && <div className='call-line'>user agent: <span className='datetime' title={model.options.userAgent}>{model.options.userAgent}</span></div>}
|
||||
<div className='call-section'>Viewport</div>
|
||||
{model.options.viewport && <div className='call-line'>width: <span className='number' title={String(!!model.options.viewport?.width)}>{model.options.viewport.width}</span></div>}
|
||||
{model.options.viewport && <div className='call-line'>height: <span className='number' title={String(!!model.options.viewport?.height)}>{model.options.viewport.height}</span></div>}
|
||||
<div className='call-line'>is mobile: <span className='boolean' title={String(!!model.options.isMobile)}>{String(!!model.options.isMobile)}</span></div>
|
||||
{model.options.deviceScaleFactor && <div className='call-line'>device scale: <span className='number' title={String(model.options.deviceScaleFactor)}>{String(model.options.deviceScaleFactor)}</span></div>}
|
||||
<div className='call-section'>Counts</div>
|
||||
<div className='call-line'>pages: <span className='number'>{model.pages.length}</span></div>
|
||||
<div className='call-line'>actions: <span className='number'>{model.actions.length}</span></div>
|
||||
<div className='call-line'>events: <span className='number'>{model.events.length}</span></div>
|
||||
</div> },
|
||||
]
|
||||
} selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/>
|
||||
</SplitView>
|
||||
<Workbench model={model} view='standalone'></Workbench>
|
||||
{!!progress.total && <div className='progress'>
|
||||
<div className='inner-progress' style={{ width: (100 * progress.done / progress.total) + '%' }}></div>
|
||||
</div>}
|
||||
|
|
@ -241,4 +170,91 @@ export const Workbench: React.FunctionComponent<{
|
|||
</div>;
|
||||
};
|
||||
|
||||
const emptyModel = new MultiTraceModel([]);
|
||||
export const Workbench: React.FunctionComponent<{
|
||||
model: MultiTraceModel,
|
||||
view: 'embedded' | 'standalone'
|
||||
}> = ({ model, view }) => {
|
||||
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
||||
const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState<string>('logs');
|
||||
|
||||
const activeAction = highlightedAction || selectedAction;
|
||||
const boundaries = { minimum: model.startTime, maximum: model.endTime };
|
||||
|
||||
// Leave some nice free space on the right hand side.
|
||||
boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20;
|
||||
const { errors, warnings } = activeAction ? modelUtil.stats(activeAction) : { errors: 0, warnings: 0 };
|
||||
const consoleCount = errors + warnings;
|
||||
const networkCount = activeAction ? modelUtil.resourcesForAction(activeAction).length : 0;
|
||||
|
||||
const tabs = [
|
||||
{ id: 'logs', title: 'Call', count: 0, render: () => <CallTab action={activeAction} sdkLanguage={model.sdkLanguage} /> },
|
||||
{ id: 'console', title: 'Console', count: consoleCount, render: () => <ConsoleTab action={activeAction} /> },
|
||||
{ id: 'network', title: 'Network', count: networkCount, render: () => <NetworkTab action={activeAction} /> },
|
||||
];
|
||||
|
||||
if (model.hasSource)
|
||||
tabs.push({ id: 'source', title: 'Source', count: 0, render: () => <SourceTab action={selectedAction} /> });
|
||||
|
||||
return <div className='vbox'>
|
||||
<div style={{ paddingLeft: '20px', flex: 'none', borderBottom: '1px solid var(--vscode-panel-border)' }}>
|
||||
<Timeline
|
||||
context={model}
|
||||
boundaries={boundaries}
|
||||
selectedAction={activeAction}
|
||||
onSelected={action => setSelectedAction(action)}
|
||||
/>
|
||||
</div>
|
||||
<SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
|
||||
<SplitView sidebarSize={300} orientation={view === 'embedded' ? 'vertical' : 'horizontal'}>
|
||||
<SnapshotTab action={activeAction} />
|
||||
<TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab}/>
|
||||
</SplitView>
|
||||
<TabbedPane tabs={
|
||||
[
|
||||
{ id: 'actions', title: 'Actions', count: 0, render: () => <ActionList
|
||||
sdkLanguage={model.sdkLanguage}
|
||||
actions={model.actions}
|
||||
selectedAction={selectedAction}
|
||||
onSelected={action => {
|
||||
setSelectedAction(action);
|
||||
}}
|
||||
onHighlighted={action => {
|
||||
setHighlightedAction(action);
|
||||
}}
|
||||
setSelectedTab={setSelectedPropertiesTab}
|
||||
/> },
|
||||
{ id: 'metadata', title: 'Metadata', count: 0, render: () => <div className='vbox'>
|
||||
<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>}
|
||||
<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-line'>engine:<span className='call-value string' title={model.browserName}>{model.browserName}</span></div>
|
||||
{model.platform && <div className='call-line'>platform:<span className='call-value string' title={model.platform}>{model.platform}</span></div>}
|
||||
{model.options.userAgent && <div className='call-line'>user agent:<span className='call-value datetime' title={model.options.userAgent}>{model.options.userAgent}</span></div>}
|
||||
<div className='call-section'>Viewport</div>
|
||||
{model.options.viewport && <div className='call-line'>width:<span className='call-value number' title={String(!!model.options.viewport?.width)}>{model.options.viewport.width}</span></div>}
|
||||
{model.options.viewport && <div className='call-line'>height:<span className='call-value number' title={String(!!model.options.viewport?.height)}>{model.options.viewport.height}</span></div>}
|
||||
<div className='call-line'>is mobile:<span className='call-value boolean' title={String(!!model.options.isMobile)}>{String(!!model.options.isMobile)}</span></div>
|
||||
{model.options.deviceScaleFactor && <div className='call-line'>device scale:<span className='call-value number' title={String(model.options.deviceScaleFactor)}>{String(model.options.deviceScaleFactor)}</span></div>}
|
||||
<div className='call-section'>Counts</div>
|
||||
<div className='call-line'>pages:<span className='call-value number'>{model.pages.length}</span></div>
|
||||
<div className='call-line'>actions:<span className='call-value number'>{model.actions.length}</span></div>
|
||||
<div className='call-line'>events:<span className='call-value number'>{model.events.length}</span></div>
|
||||
</div> },
|
||||
]
|
||||
} selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/>
|
||||
</SplitView>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const emptyModel = new MultiTraceModel([]);
|
||||
|
||||
export async function loadSingleTraceFile(url: string): Promise<MultiTraceModel> {
|
||||
const params = new URLSearchParams();
|
||||
params.set('trace', url);
|
||||
const response = await fetch(`context?${params.toString()}`);
|
||||
const contextEntry = await response.json() as ContextEntry;
|
||||
return new MultiTraceModel([contextEntry]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,3 +96,12 @@ svg {
|
|||
flex: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.codicon-check {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.codicon-close,
|
||||
.codicon-error {
|
||||
color: var(--red);
|
||||
}
|
||||
|
|
|
|||
65
packages/web/src/components/listView.css
Normal file
65
packages/web/src/components/listView.css
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.list-view {
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.list-view-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
overflow: auto;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.list-view-entry {
|
||||
display: flex;
|
||||
flex: none;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
line-height: 28px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.list-view-entry.highlighted,
|
||||
.list-view-entry.selected {
|
||||
background-color: var(--vscode-list-inactiveSelectionBackground);
|
||||
}
|
||||
|
||||
.list-view-entry.highlighted {
|
||||
background-color: var(--vscode-list-inactiveSelectionBackground);
|
||||
}
|
||||
|
||||
.list-view-content:focus .list-view-entry.selected {
|
||||
background-color: var(--vscode-list-activeSelectionBackground);
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.list-view-content:focus .list-view-entry.selected * {
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
}
|
||||
|
||||
.list-view-empty {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
146
packages/web/src/components/listView.tsx
Normal file
146
packages/web/src/components/listView.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
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 * as React from 'react';
|
||||
import './listView.css';
|
||||
|
||||
export type ListViewProps = {
|
||||
items: any[],
|
||||
itemKey: (item: any) => string,
|
||||
itemRender: (item: any) => React.ReactNode,
|
||||
itemIcon?: (item: any) => string | undefined,
|
||||
itemIndent?: (item: any) => number | undefined,
|
||||
selectedItem?: any,
|
||||
onAccepted?: (item: any) => void,
|
||||
onSelected?: (item: any) => void,
|
||||
onHighlighted?: (item: any | undefined) => void,
|
||||
showNoItemsMessage?: boolean,
|
||||
};
|
||||
|
||||
export const ListView: React.FC<ListViewProps> = ({
|
||||
items = [],
|
||||
itemKey,
|
||||
itemRender,
|
||||
itemIcon,
|
||||
itemIndent,
|
||||
selectedItem,
|
||||
onAccepted,
|
||||
onSelected,
|
||||
onHighlighted,
|
||||
showNoItemsMessage,
|
||||
}) => {
|
||||
const itemListRef = React.createRef<HTMLDivElement>();
|
||||
const [highlightedItem, setHighlightedItem] = React.useState<any>();
|
||||
|
||||
return <div className='list-view vbox'>
|
||||
<div
|
||||
className='list-view-content'
|
||||
tabIndex={0}
|
||||
onDoubleClick={() => onAccepted?.(selectedItem)}
|
||||
onKeyDown={event => {
|
||||
if (event.key === 'Enter') {
|
||||
onAccepted?.(selectedItem);
|
||||
return;
|
||||
}
|
||||
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
|
||||
return;
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const index = selectedItem ? items.indexOf(selectedItem) : -1;
|
||||
let newIndex = index;
|
||||
if (event.key === 'ArrowDown') {
|
||||
if (index === -1)
|
||||
newIndex = 0;
|
||||
else
|
||||
newIndex = Math.min(index + 1, items.length - 1);
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
if (index === -1)
|
||||
newIndex = items.length - 1;
|
||||
else
|
||||
newIndex = Math.max(index - 1, 0);
|
||||
}
|
||||
const element = itemListRef.current?.children.item(newIndex);
|
||||
scrollIntoViewIfNeeded(element);
|
||||
onSelected?.(items[newIndex]);
|
||||
}}
|
||||
ref={itemListRef}
|
||||
>
|
||||
{showNoItemsMessage && items.length === 0 && <div className='list-view-empty'>No items</div>}
|
||||
{items.map(item => <ListItemView
|
||||
key={itemKey(item)}
|
||||
icon={itemIcon?.(item)}
|
||||
indent={itemIndent?.(item)}
|
||||
isHighlighted={item === highlightedItem}
|
||||
isSelected={item === selectedItem}
|
||||
onSelected={() => onSelected?.(item)}
|
||||
onMouseEnter={() => {
|
||||
setHighlightedItem(item);
|
||||
onHighlighted?.(item);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHighlightedItem(undefined);
|
||||
onHighlighted?.(undefined);
|
||||
}}
|
||||
>
|
||||
{itemRender(item)}
|
||||
</ListItemView>)}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const ListItemView: React.FC<{
|
||||
key: string,
|
||||
icon: string | undefined,
|
||||
indent: number | undefined,
|
||||
isHighlighted: boolean,
|
||||
isSelected: boolean,
|
||||
onSelected: () => void,
|
||||
onMouseEnter: () => void,
|
||||
onMouseLeave: () => void,
|
||||
children: React.ReactNode | React.ReactNode[],
|
||||
}> = ({ key, icon, indent, onSelected, onMouseEnter, onMouseLeave, isHighlighted, isSelected, children }) => {
|
||||
const selectedSuffix = isSelected ? ' selected' : '';
|
||||
const highlightedSuffix = isHighlighted ? ' highlighted' : '';
|
||||
const divRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (divRef.current && isSelected)
|
||||
scrollIntoViewIfNeeded(divRef.current);
|
||||
}, [isSelected]);
|
||||
|
||||
return <div
|
||||
key={key}
|
||||
className={'list-view-entry' + selectedSuffix + highlightedSuffix}
|
||||
onClick={onSelected}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
ref={divRef}
|
||||
>
|
||||
{indent ? <div style={{ minWidth: indent * 16 }}></div> : undefined}
|
||||
<div className={'codicon ' + icon} style={{ minWidth: 16, marginRight: 4 }}></div>
|
||||
{typeof children === 'string' ? <div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{children}</div> : children}
|
||||
</div>;
|
||||
};
|
||||
|
||||
function scrollIntoViewIfNeeded(element?: Element | null) {
|
||||
if (!element)
|
||||
return;
|
||||
if ((element as any)?.scrollIntoViewIfNeeded)
|
||||
(element as any).scrollIntoViewIfNeeded(false);
|
||||
else
|
||||
element?.scrollIntoView();
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ export function msToString(ms: number): string {
|
|||
return '-';
|
||||
|
||||
if (ms === 0)
|
||||
return '0ms';
|
||||
return '0';
|
||||
|
||||
if (ms < 1000)
|
||||
return ms.toFixed(0) + 'ms';
|
||||
|
|
|
|||
|
|
@ -55,13 +55,13 @@ class TraceViewerPage {
|
|||
}
|
||||
|
||||
async actionIconsText(action: string) {
|
||||
const entry = await this.page.waitForSelector(`.action-entry:has-text("${action}")`);
|
||||
const entry = await this.page.waitForSelector(`.list-view-entry:has-text("${action}")`);
|
||||
await entry.waitForSelector('.action-icon-value:visible');
|
||||
return await entry.$$eval('.action-icon-value:visible', ee => ee.map(e => e.textContent));
|
||||
}
|
||||
|
||||
async actionIcons(action: string) {
|
||||
return await this.page.waitForSelector(`.action-entry:has-text("${action}") .action-icons`);
|
||||
return await this.page.waitForSelector(`.list-view-entry:has-text("${action}") .action-icons`);
|
||||
}
|
||||
|
||||
async selectAction(title: string, ordinal: number = 0) {
|
||||
|
|
|
|||
|
|
@ -143,29 +143,29 @@ test('should open console errors on click', async ({ showTraceViewer, browserNam
|
|||
expect(await traceViewer.page.waitForSelector('.console-tab')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should show params and return value', async ({ showTraceViewer, browserName }) => {
|
||||
test('should show params and return value', async ({ showTraceViewer }) => {
|
||||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
await traceViewer.selectAction('page.evaluate');
|
||||
await expect(traceViewer.callLines).toHaveText([
|
||||
/page.evaluate/,
|
||||
/wall time: [0-9/:,APM ]+/,
|
||||
/duration: [\d]+ms/,
|
||||
/expression: "\({↵ a↵ }\) => {↵ console\.log\(\'Info\'\);↵ console\.warn\(\'Warning\'\);↵ console/,
|
||||
'isFunction: true',
|
||||
'arg: {"a":"paramA","b":4}',
|
||||
'value: "return paramA"'
|
||||
/wall time:[0-9/:,APM ]+/,
|
||||
/duration:[\d]+ms/,
|
||||
/expression:"\({↵ a↵ }\) => {↵ console\.log\(\'Info\'\);↵ console\.warn\(\'Warning\'\);↵ console/,
|
||||
'isFunction:true',
|
||||
'arg:{"a":"paramA","b":4}',
|
||||
'value:"return paramA"'
|
||||
]);
|
||||
|
||||
await traceViewer.selectAction(`locator('button')`);
|
||||
await expect(traceViewer.callLines).toContainText([
|
||||
/expect.toHaveText/,
|
||||
/wall time: [0-9/:,APM ]+/,
|
||||
/duration: [\d]+ms/,
|
||||
/locator: locator\('button'\)/,
|
||||
/expression: "to.have.text"/,
|
||||
/timeout: 10000/,
|
||||
/matches: true/,
|
||||
/received: "Click"/,
|
||||
/wall time:[0-9/:,APM ]+/,
|
||||
/duration:[\d]+ms/,
|
||||
/locator:locator\('button'\)/,
|
||||
/expression:"to.have.text"/,
|
||||
/timeout:10000/,
|
||||
/matches:true/,
|
||||
/received:"Click"/,
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -174,12 +174,12 @@ test('should show null as a param', async ({ showTraceViewer, browserName }) =>
|
|||
await traceViewer.selectAction('page.evaluate', 1);
|
||||
await expect(traceViewer.callLines).toHaveText([
|
||||
/page.evaluate/,
|
||||
/wall time: [0-9/:,APM ]+/,
|
||||
/duration: [\d]+ms/,
|
||||
'expression: "() => 1 + 1"',
|
||||
'isFunction: true',
|
||||
'arg: null',
|
||||
'value: 2'
|
||||
/wall time:[0-9/:,APM ]+/,
|
||||
/duration:[\d]+ms/,
|
||||
'expression:"() => 1 + 1"',
|
||||
'isFunction:true',
|
||||
'arg:null',
|
||||
'value:2'
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -604,15 +604,15 @@ test('should include metainfo', async ({ showTraceViewer, browserName }) => {
|
|||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
await traceViewer.page.locator('text=Metadata').click();
|
||||
const callLine = traceViewer.page.locator('.call-line');
|
||||
await expect(callLine.getByText('start time')).toHaveText(/start time: [\d/,: ]+/);
|
||||
await expect(callLine.getByText('duration')).toHaveText(/duration: [\dms]+/);
|
||||
await expect(callLine.getByText('engine')).toHaveText(/engine: [\w]+/);
|
||||
await expect(callLine.getByText('platform')).toHaveText(/platform: [\w]+/);
|
||||
await expect(callLine.getByText('width')).toHaveText(/width: [\d]+/);
|
||||
await expect(callLine.getByText('height')).toHaveText(/height: [\d]+/);
|
||||
await expect(callLine.getByText('pages')).toHaveText(/pages: 1/);
|
||||
await expect(callLine.getByText('actions')).toHaveText(/actions: [\d]+/);
|
||||
await expect(callLine.getByText('events')).toHaveText(/events: [\d]+/);
|
||||
await expect(callLine.getByText('start time')).toHaveText(/start time:[\d/,: ]+/);
|
||||
await expect(callLine.getByText('duration')).toHaveText(/duration:[\dms]+/);
|
||||
await expect(callLine.getByText('engine')).toHaveText(/engine:[\w]+/);
|
||||
await expect(callLine.getByText('platform')).toHaveText(/platform:[\w]+/);
|
||||
await expect(callLine.getByText('width')).toHaveText(/width:[\d]+/);
|
||||
await expect(callLine.getByText('height')).toHaveText(/height:[\d]+/);
|
||||
await expect(callLine.getByText('pages')).toHaveText(/pages:1/);
|
||||
await expect(callLine.getByText('actions')).toHaveText(/actions:[\d]+/);
|
||||
await expect(callLine.getByText('events')).toHaveText(/events:[\d]+/);
|
||||
});
|
||||
|
||||
test('should open two trace files', async ({ context, page, request, server, showTraceViewer }, testInfo) => {
|
||||
|
|
@ -655,16 +655,16 @@ test('should open two trace files', async ({ context, page, request, server, sho
|
|||
await traceViewer.page.locator('text=Metadata').click();
|
||||
const callLine = traceViewer.page.locator('.call-line');
|
||||
// Should get metadata from the context trace
|
||||
await expect(callLine.getByText('start time')).toHaveText(/start time: [\d/,: ]+/);
|
||||
await expect(callLine.getByText('start time')).toHaveText(/start time:[\d/,: ]+/);
|
||||
// duration in the metatadata section
|
||||
await expect(callLine.getByText('duration').first()).toHaveText(/duration: [\dms]+/);
|
||||
await expect(callLine.getByText('engine')).toHaveText(/engine: [\w]+/);
|
||||
await expect(callLine.getByText('platform')).toHaveText(/platform: [\w]+/);
|
||||
await expect(callLine.getByText('width')).toHaveText(/width: [\d]+/);
|
||||
await expect(callLine.getByText('height')).toHaveText(/height: [\d]+/);
|
||||
await expect(callLine.getByText('pages')).toHaveText(/pages: 1/);
|
||||
await expect(callLine.getByText('actions')).toHaveText(/actions: 6/);
|
||||
await expect(callLine.getByText('events')).toHaveText(/events: [\d]+/);
|
||||
await expect(callLine.getByText('duration').first()).toHaveText(/duration:[\dms]+/);
|
||||
await expect(callLine.getByText('engine')).toHaveText(/engine:[\w]+/);
|
||||
await expect(callLine.getByText('platform')).toHaveText(/platform:[\w]+/);
|
||||
await expect(callLine.getByText('width')).toHaveText(/width:[\d]+/);
|
||||
await expect(callLine.getByText('height')).toHaveText(/height:[\d]+/);
|
||||
await expect(callLine.getByText('pages')).toHaveText(/pages:1/);
|
||||
await expect(callLine.getByText('actions')).toHaveText(/actions:6/);
|
||||
await expect(callLine.getByText('events')).toHaveText(/events:[\d]+/);
|
||||
});
|
||||
|
||||
test('should include requestUrl in route.fulfill', async ({ page, runAndTrace, browserName }) => {
|
||||
|
|
@ -702,7 +702,7 @@ test('should include requestUrl in route.continue', async ({ page, runAndTrace,
|
|||
await traceViewer.page.locator('.tab-label', { hasText: 'Call' }).click();
|
||||
const callLine = traceViewer.page.locator('.call-line');
|
||||
await expect(callLine.getByText('requestUrl')).toContainText('http://test.com');
|
||||
await expect(callLine.getByText(/^url: .*/)).toContainText(server.EMPTY_PAGE);
|
||||
await expect(callLine.getByText(/^url:.*/)).toContainText(server.EMPTY_PAGE);
|
||||
});
|
||||
|
||||
test('should include requestUrl in route.abort', async ({ page, runAndTrace, server }) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue