mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: more network panel polish (#26780)
This commit is contained in:
parent
34c6197f9e
commit
c209d7e708
@ -14,61 +14,12 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.network-request {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
padding: 0 3px;
|
||||
flex: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.network-request-title {
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.network-request-title-status {
|
||||
padding: 0 2px;
|
||||
border-radius: 4px;
|
||||
margin: 2px;
|
||||
line-height: 20px;
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.network-request-title-status.status-failure {
|
||||
color: var(--vscode-statusBar-foreground);
|
||||
background-color: var(--vscode-statusBarItem-errorBackground);
|
||||
}
|
||||
|
||||
.network-request-title-status.status-route {
|
||||
color: var(--vscode-statusBar-foreground);
|
||||
background-color: var(--vscode-statusBar-background);
|
||||
}
|
||||
|
||||
.network-request-title-status.status-route.api {
|
||||
color: var(--vscode-statusBar-foreground);
|
||||
background-color: var(--vscode-statusBarItem-remoteBackground);
|
||||
}
|
||||
|
||||
.network-request-title-url {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.network-request-title-content-type {
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
.network-request-details {
|
||||
width: 100%;
|
||||
user-select: text;
|
||||
line-height: 24px;
|
||||
margin-left: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.network-request-details-url {
|
||||
@ -77,7 +28,7 @@
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.network-request-headers {
|
||||
.network-request-details-headers {
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
margin-left: 10px;
|
||||
@ -88,10 +39,6 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.network-request-details-time {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.network-request .cm-wrapper {
|
||||
.network-request-details .cm-wrapper {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
@ -15,54 +15,48 @@
|
||||
*/
|
||||
|
||||
import type { ResourceSnapshot } from '@trace/snapshot';
|
||||
import { Expandable } from '@web/components/expandable';
|
||||
import * as React from 'react';
|
||||
import './networkResourceDetails.css';
|
||||
import type { Entry } from '@trace/har';
|
||||
import { msToString } from '@web/uiUtils';
|
||||
import { TabbedPane } from '@web/components/tabbedPane';
|
||||
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||
import type { Language } from '@web/components/codeMirrorWrapper';
|
||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||
|
||||
export const NetworkResource: React.FunctionComponent<{
|
||||
resource: Entry,
|
||||
}> = ({ resource }) => {
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
export const NetworkResourceDetails: React.FunctionComponent<{
|
||||
resource: ResourceSnapshot;
|
||||
onClose: () => void;
|
||||
}> = ({ resource, onClose }) => {
|
||||
const [selectedTab, setSelectedTab] = React.useState('request');
|
||||
|
||||
const { routeStatus, resourceName, contentType } = React.useMemo(() => {
|
||||
const routeStatus = formatRouteStatus(resource);
|
||||
const resourceName = resource.request.url.substring(resource.request.url.lastIndexOf('/'));
|
||||
let contentType = resource.response.content.mimeType;
|
||||
const charset = contentType.match(/^(.*);\s*charset=.*$/);
|
||||
if (charset)
|
||||
contentType = charset[1];
|
||||
return { routeStatus, resourceName, contentType };
|
||||
}, [resource]);
|
||||
|
||||
const renderTitle = React.useCallback(() => {
|
||||
return <div className='network-request-title'>
|
||||
{routeStatus && <div className={`network-request-title-status status-route ${routeStatus}`}>{routeStatus}</div> }
|
||||
{resource.response._failureText && <div className={'network-request-title-status status-failure'}>{resource.response._failureText}</div>}
|
||||
{!resource.response._failureText && <div className={'network-request-title-status ' + formatStatus(resource.response.status)}>{resource.response.status}</div>}
|
||||
<div className='network-request-title-status'>{resource.request.method}</div>
|
||||
<div className='network-request-title-url'>{resourceName}</div>
|
||||
<div className='network-request-title-content-type'>{contentType}</div>
|
||||
</div>;
|
||||
}, [contentType, resource, resourceName, routeStatus]);
|
||||
|
||||
return <div className='network-request'>
|
||||
<Expandable expanded={expanded} setExpanded={setExpanded} title={renderTitle()} expandOnTitleClick={true}>
|
||||
{expanded && <NetworkResourceDetails resource={resource} />}
|
||||
</Expandable>
|
||||
</div>;
|
||||
return <TabbedPane
|
||||
dataTestId='network-request-details'
|
||||
leftToolbar={[<ToolbarButton icon='arrow-left' title='Back' onClick={onClose}></ToolbarButton>]}
|
||||
rightToolbar={[<ToolbarButton icon='close' title='Close' onClick={onClose}></ToolbarButton>]}
|
||||
tabs={[
|
||||
{
|
||||
id: 'request',
|
||||
title: 'Request',
|
||||
render: () => <RequestTab resource={resource}/>,
|
||||
},
|
||||
{
|
||||
id: 'response',
|
||||
title: 'Response',
|
||||
render: () => <ResponseTab resource={resource}/>,
|
||||
},
|
||||
{
|
||||
id: 'body',
|
||||
title: 'Body',
|
||||
render: () => <BodyTab resource={resource}/>,
|
||||
},
|
||||
]}
|
||||
selectedTab={selectedTab}
|
||||
setSelectedTab={setSelectedTab} />;
|
||||
};
|
||||
|
||||
const NetworkResourceDetails: React.FunctionComponent<{
|
||||
resource: ResourceSnapshot,
|
||||
const RequestTab: React.FunctionComponent<{
|
||||
resource: ResourceSnapshot;
|
||||
}> = ({ resource }) => {
|
||||
const [requestBody, setRequestBody] = React.useState<{ text: string, language?: Language } | null>(null);
|
||||
const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, language?: Language } | null>(null);
|
||||
const [selectedTab, setSelectedTab] = React.useState('request');
|
||||
|
||||
React.useEffect(() => {
|
||||
const readResources = async () => {
|
||||
@ -77,7 +71,36 @@ const NetworkResourceDetails: React.FunctionComponent<{
|
||||
setRequestBody({ text: formatBody(resource.request.postData.text, requestContentType), language });
|
||||
}
|
||||
}
|
||||
};
|
||||
readResources();
|
||||
}, [resource]);
|
||||
|
||||
return <div className='network-request-details'>
|
||||
<div className='network-request-details-header'>URL</div>
|
||||
<div className='network-request-details-url'>{resource.request.url}</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>
|
||||
{requestBody && <div className='network-request-details-header'>Request Body</div>}
|
||||
{requestBody && <CodeMirrorWrapper text={requestBody.text} language={requestBody.language} readOnly lineNumbers={true}/>}
|
||||
</div>;
|
||||
};
|
||||
|
||||
const ResponseTab: React.FunctionComponent<{
|
||||
resource: ResourceSnapshot;
|
||||
}> = ({ resource }) => {
|
||||
return <div className='network-request-details'>
|
||||
<div className='network-request-details-header'>Response Headers</div>
|
||||
<div className='network-request-details-headers'>{resource.response.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const BodyTab: React.FunctionComponent<{
|
||||
resource: ResourceSnapshot;
|
||||
}> = ({ resource }) => {
|
||||
const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, language?: Language } | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const readResources = async () => {
|
||||
if (resource.response.content._sha1) {
|
||||
const useBase64 = resource.response.content.mimeType.includes('image');
|
||||
const response = await fetch(`sha1/${resource.response.content._sha1}`);
|
||||
@ -98,48 +121,13 @@ const NetworkResourceDetails: React.FunctionComponent<{
|
||||
readResources();
|
||||
}, [resource]);
|
||||
|
||||
return <TabbedPane tabs={[
|
||||
{
|
||||
id: 'request',
|
||||
title: 'Request',
|
||||
render: () => <div className='network-request-details'>
|
||||
<div className='network-request-details-header'>URL</div>
|
||||
<div className='network-request-details-url'>{resource.request.url}</div>
|
||||
<div className='network-request-details-header'>Request Headers</div>
|
||||
<div className='network-request-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
||||
{requestBody && <div className='network-request-details-header'>Request Body</div>}
|
||||
{requestBody && <CodeMirrorWrapper text={requestBody.text} language={requestBody.language} readOnly/>}
|
||||
</div>,
|
||||
},
|
||||
{
|
||||
id: 'response',
|
||||
title: 'Response',
|
||||
render: () => <div className='network-request-details'>
|
||||
<div className='network-request-details-time'>{msToString(resource.time)}</div>
|
||||
<div className='network-request-details-header'>Response Headers</div>
|
||||
<div className='network-request-headers'>{resource.response.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
||||
</div>,
|
||||
},
|
||||
{
|
||||
id: 'body',
|
||||
title: 'Body',
|
||||
render: () => <div className='network-request-details'>
|
||||
{!resource.response.content._sha1 && <div>Response body is not available for this request.</div>}
|
||||
{responseBody && responseBody.dataUrl && <img draggable='false' src={responseBody.dataUrl} />}
|
||||
{responseBody && responseBody.text && <CodeMirrorWrapper text={responseBody.text} language={responseBody.language} readOnly/>}
|
||||
</div>,
|
||||
},
|
||||
]} selectedTab={selectedTab} setSelectedTab={setSelectedTab}/>;
|
||||
return <div className='network-request-details'>
|
||||
{!resource.response.content._sha1 && <div>Response body is not available for this request.</div>}
|
||||
{responseBody && responseBody.dataUrl && <img draggable='false' src={responseBody.dataUrl} />}
|
||||
{responseBody && responseBody.text && <CodeMirrorWrapper text={responseBody.text} language={responseBody.language} readOnly lineNumbers={true}/>}
|
||||
</div>;
|
||||
};
|
||||
|
||||
function formatStatus(status: number): string {
|
||||
if (status >= 200 && status < 400)
|
||||
return 'status-success';
|
||||
if (status >= 400)
|
||||
return 'status-failure';
|
||||
return '';
|
||||
}
|
||||
|
||||
function formatBody(body: string | null, contentType: string): string {
|
||||
if (body === null)
|
||||
return 'Loading...';
|
||||
@ -162,18 +150,6 @@ function formatBody(body: string | null, contentType: string): string {
|
||||
return bodyStr;
|
||||
}
|
||||
|
||||
function formatRouteStatus(request: Entry): string {
|
||||
if (request._wasAborted)
|
||||
return 'aborted';
|
||||
if (request._wasContinued)
|
||||
return 'continued';
|
||||
if (request._wasFulfilled)
|
||||
return 'fulfilled';
|
||||
if (request._apiRequest)
|
||||
return 'api';
|
||||
return '';
|
||||
}
|
||||
|
||||
function mimeTypeToHighlighter(mimeType: string): Language | undefined {
|
||||
if (mimeType.includes('javascript') || mimeType.includes('json'))
|
||||
return 'javascript';
|
||||
|
||||
@ -14,17 +14,113 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.network-tab {
|
||||
.network-request-status .status-failure {
|
||||
color: var(--vscode-statusBar-foreground);
|
||||
background-color: var(--vscode-statusBarItem-errorBackground);
|
||||
}
|
||||
|
||||
.network-request-status .status-route {
|
||||
color: var(--vscode-statusBar-foreground);
|
||||
background-color: var(--vscode-statusBar-background);
|
||||
}
|
||||
|
||||
.network-request-status .status-route.api {
|
||||
color: var(--vscode-statusBar-foreground);
|
||||
background-color: var(--vscode-statusBarItem-remoteBackground);
|
||||
}
|
||||
|
||||
.network-request-status {
|
||||
flex: 0 0 70px;
|
||||
}
|
||||
|
||||
.network-request-method {
|
||||
flex: 0 0 70px;
|
||||
}
|
||||
|
||||
.network-request-file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.network-tab:focus {
|
||||
outline: none;
|
||||
.network-request-file-url {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.network-request .expandable {
|
||||
width: 100%;
|
||||
}
|
||||
.network-request-content-type,
|
||||
.network-request-time,
|
||||
.network-request-route,
|
||||
.network-request-size {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 0.5;
|
||||
}
|
||||
|
||||
.network-request-route {
|
||||
flex: 0.25;
|
||||
}
|
||||
|
||||
.network-request-header {
|
||||
height: 28px;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
flex: none;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.network-request-header .codicon-triangle-up {
|
||||
display: none;
|
||||
}
|
||||
.network-request-header .codicon-triangle-down {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.network-request-header > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.network-request-header.filter-status.positive .network-request-status .codicon-triangle-down {
|
||||
display: initial !important;
|
||||
}
|
||||
.network-request-header.filter-status.negative .network-request-status .codicon-triangle-up {
|
||||
display: initial !important;
|
||||
}
|
||||
|
||||
.network-request-header.filter-method.positive .network-request-method .codicon-triangle-down {
|
||||
display: initial !important;
|
||||
}
|
||||
.network-request-header.filter-method.negative .network-request-method .codicon-triangle-up {
|
||||
display: initial !important;
|
||||
}
|
||||
|
||||
.network-request-header.filter-file.positive .network-request-file .codicon-triangle-down {
|
||||
display: initial !important;
|
||||
}
|
||||
.network-request-header.filter-file.negative .network-request-file .codicon-triangle-up {
|
||||
display: initial !important;
|
||||
}
|
||||
|
||||
.network-request-header.filter-content-type.positive .network-request-content-type .codicon-triangle-down {
|
||||
display: initial !important;
|
||||
}
|
||||
.network-request-header.filter-content-type.negative .network-request-content-type .codicon-triangle-up {
|
||||
display: initial !important;
|
||||
}
|
||||
|
||||
.network-request-header.filter-time.positive .network-request-time .codicon-triangle-down {
|
||||
display: initial !important;
|
||||
}
|
||||
.network-request-header.filter-time.negative .network-request-time .codicon-triangle-up {
|
||||
display: initial !important;
|
||||
}
|
||||
|
||||
.network-request-header.filter-size.positive .network-request-size .codicon-triangle-down {
|
||||
display: initial !important;
|
||||
}
|
||||
.network-request-header.filter-size.negative .network-request-size .codicon-triangle-up {
|
||||
display: initial !important;
|
||||
}
|
||||
|
||||
@ -14,25 +14,198 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Entry } from '@trace/har';
|
||||
import { ListView } from '@web/components/listView';
|
||||
import * as React from 'react';
|
||||
import type * as modelUtil from './modelUtil';
|
||||
import { NetworkResource } from './networkResourceDetails';
|
||||
import './networkTab.css';
|
||||
import type { Boundaries } from '../geometry';
|
||||
import type * as modelUtil from './modelUtil';
|
||||
import './networkTab.css';
|
||||
import { NetworkResourceDetails } from './networkResourceDetails';
|
||||
import { bytesToString, msToString } from '@web/uiUtils';
|
||||
|
||||
const NetworkListView = ListView<Entry>;
|
||||
|
||||
type Filter = 'status' | 'method' | 'file' | 'time' | 'size' | 'content-type';
|
||||
|
||||
export const NetworkTab: React.FunctionComponent<{
|
||||
model: modelUtil.MultiTraceModel | undefined,
|
||||
selectedTime: Boundaries | undefined,
|
||||
}> = ({ model, selectedTime }) => {
|
||||
const [resource, setResource] = React.useState<Entry | undefined>();
|
||||
const [filter, setFilter] = React.useState<Filter | undefined>(undefined);
|
||||
const [negateFilter, setNegateFilter] = React.useState<boolean>(false);
|
||||
|
||||
const resources = React.useMemo(() => {
|
||||
const resources = model?.resources || [];
|
||||
if (!selectedTime)
|
||||
return resources;
|
||||
return resources.filter(resource => {
|
||||
const filtered = resources.filter(resource => {
|
||||
if (!selectedTime)
|
||||
return true;
|
||||
return !!resource._monotonicTime && (resource._monotonicTime >= selectedTime.minimum && resource._monotonicTime <= selectedTime.maximum);
|
||||
});
|
||||
}, [model, selectedTime]);
|
||||
return <div className='network-tab'> {
|
||||
resources.map((resource, index) => <NetworkResource key={index} resource={resource}></NetworkResource>)
|
||||
}</div>;
|
||||
if (filter)
|
||||
sort(filtered, filter, negateFilter);
|
||||
return filtered;
|
||||
}, [filter, model, negateFilter, selectedTime]);
|
||||
|
||||
const toggleFilter = React.useCallback((f: Filter) => {
|
||||
if (filter === f) {
|
||||
setNegateFilter(!negateFilter);
|
||||
} else {
|
||||
setNegateFilter(false);
|
||||
setFilter(f);
|
||||
}
|
||||
}, [filter, negateFilter]);
|
||||
|
||||
return <>
|
||||
{!resource && <div className='vbox'>
|
||||
<NetworkHeader filter={filter} negateFilter={negateFilter} toggleFilter={toggleFilter} />
|
||||
<NetworkListView
|
||||
dataTestId='network-request-list'
|
||||
items={resources}
|
||||
render={entry => <NetworkResource resource={entry}></NetworkResource>}
|
||||
onSelected={setResource}
|
||||
/>
|
||||
</div>}
|
||||
{resource && <NetworkResourceDetails resource={resource} onClose={() => setResource(undefined)} />}
|
||||
</>;
|
||||
};
|
||||
|
||||
const NetworkHeader: React.FunctionComponent<{
|
||||
filter: Filter | undefined,
|
||||
negateFilter: boolean,
|
||||
toggleFilter: (filter: Filter) => void,
|
||||
}> = ({ toggleFilter, filter, negateFilter }) => {
|
||||
return <div className={'hbox network-request-header' + (filter ? ' filter-' + filter : '') + (negateFilter ? ' negative' : ' positive')}>
|
||||
<div className='network-request-status' onClick={() => toggleFilter('status') }>
|
||||
Status
|
||||
<span className='codicon codicon-triangle-up' />
|
||||
<span className='codicon codicon-triangle-down' />
|
||||
</div>
|
||||
<div className='network-request-method' onClick={() => toggleFilter('method') }>
|
||||
Method
|
||||
<span className='codicon codicon-triangle-up' />
|
||||
<span className='codicon codicon-triangle-down' />
|
||||
</div>
|
||||
<div className='network-request-file' onClick={() => toggleFilter('file') }>
|
||||
Request
|
||||
<span className='codicon codicon-triangle-up' />
|
||||
<span className='codicon codicon-triangle-down' />
|
||||
</div>
|
||||
<div className='network-request-content-type' onClick={() => toggleFilter('content-type') }>
|
||||
Content Type
|
||||
<span className='codicon codicon-triangle-up' />
|
||||
<span className='codicon codicon-triangle-down' />
|
||||
</div>
|
||||
<div className='network-request-time' onClick={() => toggleFilter('time') }>
|
||||
Time
|
||||
<span className='codicon codicon-triangle-up' />
|
||||
<span className='codicon codicon-triangle-down' />
|
||||
</div>
|
||||
<div className='network-request-size' onClick={() => toggleFilter('size') }>
|
||||
Size
|
||||
<span className='codicon codicon-triangle-up' />
|
||||
<span className='codicon codicon-triangle-down' />
|
||||
</div>
|
||||
<div className='network-request-route'>Route</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const NetworkResource: React.FunctionComponent<{
|
||||
resource: Entry,
|
||||
}> = ({ resource }) => {
|
||||
const { routeStatus, resourceName, contentType } = React.useMemo(() => {
|
||||
const routeStatus = formatRouteStatus(resource);
|
||||
const resourceName = resource.request.url.substring(resource.request.url.lastIndexOf('/'));
|
||||
let contentType = resource.response.content.mimeType;
|
||||
const charset = contentType.match(/^(.*);\s*charset=.*$/);
|
||||
if (charset)
|
||||
contentType = charset[1];
|
||||
return { routeStatus, resourceName, contentType };
|
||||
}, [resource]);
|
||||
|
||||
return <div className='hbox'>
|
||||
<div className='hbox network-request-status'>
|
||||
<div className={formatStatus(resource.response.status)} title={resource.response.statusText}>{resource.response.status}</div>
|
||||
</div>
|
||||
<div className='hbox network-request-method'>
|
||||
<div>{resource.request.method}</div>
|
||||
</div>
|
||||
<div className='network-request-file'>
|
||||
<div className='network-request-file-url' title={resource.request.url}>{resourceName}</div>
|
||||
</div>
|
||||
<div className='network-request-content-type' title={contentType}>{contentType}</div>
|
||||
<div className='network-request-time'>{msToString(resource.time)}</div>
|
||||
<div className='network-request-size'>{bytesToString(resource.response._transferSize! > 0 ? resource.response._transferSize! : resource.response.bodySize)}</div>
|
||||
<div className='network-request-route'>
|
||||
{routeStatus && <div className={`status-route ${routeStatus}`}>{routeStatus}</div>}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
function formatStatus(status: number): string {
|
||||
if (status >= 200 && status < 400)
|
||||
return 'status-success';
|
||||
if (status >= 400)
|
||||
return 'status-failure';
|
||||
return '';
|
||||
}
|
||||
|
||||
function formatRouteStatus(request: Entry): string {
|
||||
if (request._wasAborted)
|
||||
return 'aborted';
|
||||
if (request._wasContinued)
|
||||
return 'continued';
|
||||
if (request._wasFulfilled)
|
||||
return 'fulfilled';
|
||||
if (request._apiRequest)
|
||||
return 'api';
|
||||
return '';
|
||||
}
|
||||
|
||||
function sort(resources: Entry[], filter: Filter | undefined, negate: boolean) {
|
||||
const c = comparator(filter);
|
||||
if (c)
|
||||
resources.sort(c);
|
||||
if (negate)
|
||||
resources.reverse();
|
||||
}
|
||||
|
||||
function comparator(filter: Filter | undefined) {
|
||||
if (filter === 'time')
|
||||
return (a: Entry, b: Entry) => a.time - b.time;
|
||||
|
||||
if (filter === 'status')
|
||||
return (a: Entry, b: Entry) => a.response.status - b.response.status;
|
||||
|
||||
if (filter === 'method') {
|
||||
return (a: Entry, b: Entry) => {
|
||||
const valueA = a.request.method;
|
||||
const valueB = b.request.method;
|
||||
return valueA.localeCompare(valueB);
|
||||
};
|
||||
}
|
||||
|
||||
if (filter === 'size') {
|
||||
return (a: Entry, b: Entry) => {
|
||||
const sizeA = a.response._transferSize! > 0 ? a.response._transferSize! : a.response.bodySize;
|
||||
const sizeB = b.response._transferSize! > 0 ? b.response._transferSize! : b.response.bodySize;
|
||||
return sizeA - sizeB;
|
||||
};
|
||||
}
|
||||
|
||||
if (filter === 'content-type') {
|
||||
return (a: Entry, b: Entry) => {
|
||||
const valueA = a.response.content.mimeType;
|
||||
const valueB = b.response.content.mimeType;
|
||||
return valueA.localeCompare(valueB);
|
||||
};
|
||||
}
|
||||
|
||||
if (filter === 'file') {
|
||||
return (a: Entry, b: Entry) => {
|
||||
const nameA = a.request.url.substring(a.request.url.lastIndexOf('/'));
|
||||
const nameB = b.request.url.substring(b.request.url.lastIndexOf('/'));
|
||||
return nameA.localeCompare(nameB);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,12 +30,13 @@ export const TabbedPane: React.FunctionComponent<{
|
||||
leftToolbar?: React.ReactElement[],
|
||||
rightToolbar?: React.ReactElement[],
|
||||
selectedTab: string,
|
||||
setSelectedTab: (tab: string) => void
|
||||
}> = ({ tabs, selectedTab, setSelectedTab, leftToolbar, rightToolbar }) => {
|
||||
return <div className='tabbed-pane'>
|
||||
setSelectedTab: (tab: string) => void,
|
||||
dataTestId?: string,
|
||||
}> = ({ tabs, selectedTab, setSelectedTab, leftToolbar, rightToolbar, dataTestId }) => {
|
||||
return <div className='tabbed-pane' data-testid={dataTestId}>
|
||||
<div className='vbox'>
|
||||
<Toolbar>
|
||||
{ leftToolbar && <div style={{ flex: 'none', display: 'flex', margin: '0 4px' }}>
|
||||
{ leftToolbar && <div style={{ flex: 'none', display: 'flex', margin: '0 4px', alignItems: 'center' }}>
|
||||
{...leftToolbar}
|
||||
</div>}
|
||||
<div style={{ flex: 'auto', display: 'flex', height: '100%', overflow: 'hidden' }}>
|
||||
@ -48,7 +49,7 @@ export const TabbedPane: React.FunctionComponent<{
|
||||
></TabbedPaneTab>)),
|
||||
]}
|
||||
</div>
|
||||
{rightToolbar && <div style={{ flex: 'none', display: 'flex' }}>
|
||||
{rightToolbar && <div style={{ flex: 'none', display: 'flex', alignItems: 'center' }}>
|
||||
{...rightToolbar}
|
||||
</div>}
|
||||
</Toolbar>
|
||||
|
||||
@ -80,6 +80,28 @@ export function msToString(ms: number): string {
|
||||
return days.toFixed(1) + 'd';
|
||||
}
|
||||
|
||||
export function bytesToString(bytes: number): string {
|
||||
if (bytes < 0 || !isFinite(bytes))
|
||||
return '-';
|
||||
|
||||
if (bytes === 0)
|
||||
return '0';
|
||||
|
||||
if (bytes < 1000)
|
||||
return bytes.toFixed(0);
|
||||
|
||||
const kb = bytes / 1024;
|
||||
if (kb < 1000)
|
||||
return kb.toFixed(1) + 'K';
|
||||
|
||||
const mb = kb / 1024;
|
||||
if (mb < 1000)
|
||||
return mb.toFixed(1) + 'M';
|
||||
|
||||
const gb = mb / 1024;
|
||||
return gb.toFixed(1) + 'G';
|
||||
}
|
||||
|
||||
export function lowerBound<S, T>(array: S[], object: T, comparator: (object: T, b: S) => number, left?: number, right?: number): number {
|
||||
let l = left || 0;
|
||||
let r = right !== undefined ? right : array.length;
|
||||
|
||||
@ -51,7 +51,7 @@ class TraceViewerPage {
|
||||
this.consoleLineMessages = page.locator('.console-line-message');
|
||||
this.consoleStacks = page.locator('.console-stack');
|
||||
this.stackFrames = page.getByTestId('stack-trace').locator('.list-view-entry');
|
||||
this.networkRequests = page.locator('.network-request-title');
|
||||
this.networkRequests = page.getByTestId('network-request-list').locator('.list-view-entry');
|
||||
this.snapshotContainer = page.locator('.snapshot-container iframe.snapshot-visible[name=snapshot]');
|
||||
}
|
||||
|
||||
|
||||
@ -253,7 +253,7 @@ test('should have network request overrides', async ({ page, server, runAndTrace
|
||||
await traceViewer.selectAction('http://localhost');
|
||||
await traceViewer.showNetworkTab();
|
||||
await expect(traceViewer.networkRequests).toContainText([/200GET\/frame.htmltext\/html/]);
|
||||
await expect(traceViewer.networkRequests).toContainText([/aborted.*style.cssx-unknown/]);
|
||||
await expect(traceViewer.networkRequests).toContainText([/GET\/style.cssx-unknown.*aborted/]);
|
||||
await expect(traceViewer.networkRequests).not.toContainText([/continued/]);
|
||||
});
|
||||
|
||||
@ -264,8 +264,8 @@ test('should have network request overrides 2', async ({ page, server, runAndTra
|
||||
});
|
||||
await traceViewer.selectAction('http://localhost');
|
||||
await traceViewer.showNetworkTab();
|
||||
await expect(traceViewer.networkRequests).toContainText([/200GET\/frame.htmltext\/html/]);
|
||||
await expect(traceViewer.networkRequests).toContainText([/continued.*script.jsapplication\/javascript/]);
|
||||
await expect.soft(traceViewer.networkRequests).toContainText([/200GET\/frame.htmltext\/html.*/]);
|
||||
await expect.soft(traceViewer.networkRequests).toContainText([/200GET\/script.jsapplication\/javascript.*continued/]);
|
||||
});
|
||||
|
||||
test('should show snapshot URL', async ({ page, runAndTrace, server }) => {
|
||||
|
||||
@ -446,7 +446,7 @@ test('merge into list report by default', async ({ runInlineTest, mergeReports }
|
||||
|
||||
const text = stripAnsi(output);
|
||||
expect(text).toContain('Running 10 tests using 3 workers');
|
||||
const lines = text.split('\n').filter(l => l.match(/^\d :/)).map(l => l.replace(/\d+ms/, 'Xms'));
|
||||
const lines = text.split('\n').filter(l => l.match(/^\d :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms'));
|
||||
expect(lines).toEqual([
|
||||
`0 : 1 a.test.js:3:11 › math 1`,
|
||||
`0 : ${POSITIVE_STATUS_MARK} 1 a.test.js:3:11 › math 1 (Xms)`,
|
||||
|
||||
@ -72,7 +72,7 @@ for (const useIntermediateMergeReport of [false, true] as const) {
|
||||
`,
|
||||
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PW_TEST_DEBUG_REPORTERS_PRINT_STEPS: '1', PWTEST_TTY_WIDTH: '80' });
|
||||
const text = result.output;
|
||||
const lines = text.split('\n').filter(l => l.match(/^\d :/)).map(l => l.replace(/\d+ms/, 'Xms'));
|
||||
const lines = text.split('\n').filter(l => l.match(/^\d :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms'));
|
||||
lines.pop(); // Remove last item that contains [v] and time in ms.
|
||||
expect(lines).toEqual([
|
||||
'0 : 1 a.test.ts:3:15 › passes',
|
||||
@ -107,7 +107,7 @@ for (const useIntermediateMergeReport of [false, true] as const) {
|
||||
});`,
|
||||
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PWTEST_TTY_WIDTH: '80' });
|
||||
const text = result.output;
|
||||
const lines = text.split('\n').filter(l => l.match(/^\d :/)).map(l => l.replace(/\d+ms/, 'Xms'));
|
||||
const lines = text.split('\n').filter(l => l.match(/^\d :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms'));
|
||||
lines.pop(); // Remove last item that contains [v] and time in ms.
|
||||
expect(lines).toEqual([
|
||||
'0 : 1 a.test.ts:3:11 › passes',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user