mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: render route markers in the trace network panel (#23476)
Fixes https://github.com/microsoft/playwright/issues/23040 
This commit is contained in:
parent
3c2a8fa306
commit
5a14619bab
@ -52,6 +52,9 @@ export abstract class BrowserContext extends SdkObject {
|
||||
Response: 'response',
|
||||
RequestFailed: 'requestfailed',
|
||||
RequestFinished: 'requestfinished',
|
||||
RequestAborted: 'requestaborted',
|
||||
RequestFulfilled: 'requestfulfilled',
|
||||
RequestContinued: 'requestcontinued',
|
||||
BeforeClose: 'beforeclose',
|
||||
VideoStarted: 'videostarted',
|
||||
};
|
||||
|
||||
@ -101,7 +101,11 @@ export class HarTracer {
|
||||
eventsHelper.addEventListener(this._context, BrowserContext.Events.Request, (request: network.Request) => this._onRequest(request)),
|
||||
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFinished, ({ request, response }) => this._onRequestFinished(request, response).catch(() => {})),
|
||||
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFailed, request => this._onRequestFailed(request)),
|
||||
eventsHelper.addEventListener(this._context, BrowserContext.Events.Response, (response: network.Response) => this._onResponse(response)));
|
||||
eventsHelper.addEventListener(this._context, BrowserContext.Events.Response, (response: network.Response) => this._onResponse(response)),
|
||||
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestAborted, request => this._onRequestAborted(request)),
|
||||
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFulfilled, request => this._onRequestFulfilled(request)),
|
||||
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestContinued, request => this._onRequestContinued(request)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,6 +381,24 @@ export class HarTracer {
|
||||
this._delegate.onEntryFinished(harEntry);
|
||||
}
|
||||
|
||||
private _onRequestAborted(request: network.Request) {
|
||||
const harEntry = this._entryForRequest(request);
|
||||
if (harEntry)
|
||||
harEntry._wasAborted = true;
|
||||
}
|
||||
|
||||
private _onRequestFulfilled(request: network.Request) {
|
||||
const harEntry = this._entryForRequest(request);
|
||||
if (harEntry)
|
||||
harEntry._wasFulfilled = true;
|
||||
}
|
||||
|
||||
private _onRequestContinued(request: network.Request) {
|
||||
const harEntry = this._entryForRequest(request);
|
||||
if (harEntry)
|
||||
harEntry._wasContinued = true;
|
||||
}
|
||||
|
||||
private _storeResponseContent(buffer: Buffer | undefined, content: har.Content, resourceType: string) {
|
||||
if (!buffer) {
|
||||
content.size = 0;
|
||||
|
||||
@ -25,6 +25,7 @@ import { SdkObject } from './instrumentation';
|
||||
import type { HeadersArray, NameValue } from '../common/types';
|
||||
import { APIRequestContext } from './fetch';
|
||||
import type { NormalizedContinueOverrides } from './types';
|
||||
import { BrowserContext } from './browserContext';
|
||||
|
||||
export function filterCookies(cookies: channels.NetworkCookie[], urls: string[]): channels.NetworkCookie[] {
|
||||
const parsedURLs = urls.map(s => new URL(s));
|
||||
@ -257,6 +258,7 @@ export class Route extends SdkObject {
|
||||
async abort(errorCode: string = 'failed') {
|
||||
this._startHandling();
|
||||
await this._delegate.abort(errorCode);
|
||||
this._request._context.emit(BrowserContext.Events.RequestAborted, this._request);
|
||||
this._endHandling();
|
||||
}
|
||||
|
||||
@ -289,6 +291,7 @@ export class Route extends SdkObject {
|
||||
body,
|
||||
isBase64,
|
||||
});
|
||||
this._request._context.emit(BrowserContext.Events.RequestFulfilled, this._request);
|
||||
this._endHandling();
|
||||
}
|
||||
|
||||
@ -320,6 +323,8 @@ export class Route extends SdkObject {
|
||||
}
|
||||
this._request._setOverrides(overrides);
|
||||
await this._delegate.continue(this._request, overrides);
|
||||
if (Object.values(overrides).some(value => value !== undefined))
|
||||
this._request._context.emit(BrowserContext.Events.RequestContinued, this._request);
|
||||
this._endHandling();
|
||||
}
|
||||
|
||||
|
||||
@ -34,18 +34,21 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.network-request-title-status,
|
||||
.network-request-title-method {
|
||||
padding-right: 5px;
|
||||
.network-request-title-status {
|
||||
padding: 0 2px;
|
||||
border-radius: 4px;
|
||||
margin: 2px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.network-request-title-status.status-failure {
|
||||
background-color: var(--red);
|
||||
color: var(--white);
|
||||
color: var(--vscode-statusBar-foreground);
|
||||
background-color: var(--vscode-statusBarItem-errorBackground);
|
||||
}
|
||||
|
||||
.network-request-title-status.status-neutral {
|
||||
background-color: var(--white);
|
||||
.network-request-title-status.status-route {
|
||||
color: var(--vscode-statusBar-foreground);
|
||||
background-color: var(--vscode-statusBar-background);
|
||||
}
|
||||
|
||||
.network-request-title-url {
|
||||
@ -54,6 +57,10 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.network-request-title-content-type {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.network-request-details {
|
||||
width: 100%;
|
||||
user-select: text;
|
||||
|
||||
@ -18,6 +18,7 @@ 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';
|
||||
|
||||
export const NetworkResourceDetails: React.FunctionComponent<{
|
||||
resource: ResourceSnapshot,
|
||||
@ -64,64 +65,28 @@ export const NetworkResourceDetails: React.FunctionComponent<{
|
||||
readResources();
|
||||
}, [expanded, resource]);
|
||||
|
||||
function formatBody(body: string | null, contentType: string): string {
|
||||
if (body === null)
|
||||
return 'Loading...';
|
||||
const { routeStatus, requestContentType, resourceName, contentType } = React.useMemo(() => {
|
||||
const routeStatus = formatRouteStatus(resource);
|
||||
const requestContentTypeHeader = resource.request.headers.find(q => q.name === 'Content-Type');
|
||||
const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : '';
|
||||
const resourceName = resource.request.url.substring(resource.request.url.lastIndexOf('/') + 1);
|
||||
let contentType = resource.response.content.mimeType;
|
||||
const charset = contentType.match(/^(.*);\s*charset=.*$/);
|
||||
if (charset)
|
||||
contentType = charset[1];
|
||||
return { routeStatus, requestContentType, resourceName, contentType };
|
||||
}, [resource]);
|
||||
|
||||
const bodyStr = body;
|
||||
|
||||
if (bodyStr === '')
|
||||
return '<Empty>';
|
||||
|
||||
if (contentType.includes('application/json')) {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(bodyStr), null, 2);
|
||||
} catch (err) {
|
||||
return bodyStr;
|
||||
}
|
||||
}
|
||||
|
||||
if (contentType.includes('application/x-www-form-urlencoded'))
|
||||
return decodeURIComponent(bodyStr);
|
||||
|
||||
return bodyStr;
|
||||
}
|
||||
|
||||
function formatStatus(status: number): string {
|
||||
if (status >= 200 && status < 400)
|
||||
return 'status-success';
|
||||
|
||||
if (status >= 400)
|
||||
return 'status-failure';
|
||||
|
||||
return 'status-neutral';
|
||||
}
|
||||
|
||||
const requestContentTypeHeader = resource.request.headers.find(q => q.name === 'Content-Type');
|
||||
const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : '';
|
||||
const resourceName = resource.request.url.substring(resource.request.url.lastIndexOf('/') + 1);
|
||||
|
||||
let contentType = resource.response.content.mimeType;
|
||||
const charset = contentType.match(/^(.*);\s*charset=.*$/);
|
||||
if (charset)
|
||||
contentType = charset[1];
|
||||
|
||||
const renderTitle = () => {
|
||||
if (resource.response._failureText) {
|
||||
return <div className='network-request-title'>
|
||||
<div className={'network-request-title-status status-failure'}>{resource.response._failureText}</div>
|
||||
<div className='network-request-title-method'>{resource.request.method}</div>
|
||||
<div className='network-request-title-url'>{resource.request.url}</div>
|
||||
</div>;
|
||||
} else {
|
||||
return <div className='network-request-title'>
|
||||
<div className={'network-request-title-status ' + formatStatus(resource.response.status)}>{resource.response.status}</div>
|
||||
<div className='network-request-title-method'>{resource.request.method}</div>
|
||||
<div className='network-request-title-url'>{resourceName}</div>
|
||||
<div className='network-request-title-content-type'>{contentType}</div>
|
||||
</div>;
|
||||
}
|
||||
};
|
||||
const renderTitle = React.useCallback(() => {
|
||||
return <div className='network-request-title'>
|
||||
{routeStatus && <div className={'network-request-title-status status-route'}>{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 ' + (selected ? 'selected' : '')} onClick={() => setSelected(index)}>
|
||||
@ -144,3 +109,43 @@ export const NetworkResourceDetails: React.FunctionComponent<{
|
||||
</Expandable>
|
||||
</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...';
|
||||
|
||||
const bodyStr = body;
|
||||
if (bodyStr === '')
|
||||
return '<Empty>';
|
||||
|
||||
if (contentType.includes('application/json')) {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(bodyStr), null, 2);
|
||||
} catch (err) {
|
||||
return bodyStr;
|
||||
}
|
||||
}
|
||||
|
||||
if (contentType.includes('application/x-www-form-urlencoded'))
|
||||
return decodeURIComponent(bodyStr);
|
||||
|
||||
return bodyStr;
|
||||
}
|
||||
|
||||
function formatRouteStatus(request: Entry): string {
|
||||
if (request._wasAborted)
|
||||
return 'aborted';
|
||||
if (request._wasContinued)
|
||||
return 'continued';
|
||||
if (request._wasFulfilled)
|
||||
return 'fulfilled';
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -68,6 +68,9 @@ export type Entry = {
|
||||
_monotonicTime?: number;
|
||||
_serverPort?: number;
|
||||
_securityDetails?: SecurityDetails;
|
||||
_wasAborted?: boolean;
|
||||
_wasFulfilled?: boolean;
|
||||
_wasContinued?: boolean;
|
||||
};
|
||||
|
||||
export type Request = {
|
||||
|
||||
@ -31,7 +31,8 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s
|
||||
const context = await browser.newContext();
|
||||
await context.tracing.start({ name: 'test', screenshots: true, snapshots: true, sources: true });
|
||||
const page = await context.newPage();
|
||||
await page.goto('data:text/html,<html>Hello world</html>');
|
||||
await page.route('**/style.css', route => route.abort());
|
||||
await page.goto(`data:text/html,<html>Hello world</html>`);
|
||||
await page.setContent('<button>Click</button>');
|
||||
await expect(page.locator('button')).toHaveText('Click');
|
||||
await expect(page.getByTestId('amazing-btn')).toBeHidden();
|
||||
@ -101,6 +102,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
await expect(traceViewer.actionTitles).toHaveText([
|
||||
/browserContext.newPage/,
|
||||
/page.route/,
|
||||
/page.gotodata:text\/html,<html>Hello world<\/html>/,
|
||||
/page.setContent/,
|
||||
/expect.toHaveTextlocator\('button'\)/,
|
||||
@ -113,6 +115,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||
/page.waitForResponse/,
|
||||
/page.waitForTimeout/,
|
||||
/page.gotohttp:\/\/localhost:\d+\/frames\/frame.html/,
|
||||
/route.abort/,
|
||||
/page.setViewportSize/,
|
||||
]);
|
||||
});
|
||||
@ -216,9 +219,9 @@ test('should have network requests', async ({ showTraceViewer }) => {
|
||||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
await traceViewer.selectAction('http://localhost');
|
||||
await traceViewer.showNetworkTab();
|
||||
await expect(traceViewer.networkRequests).toContainText(['200GETframe.htmltext/html']);
|
||||
await expect(traceViewer.networkRequests).toContainText(['200GETstyle.csstext/css']);
|
||||
await expect(traceViewer.networkRequests).toContainText(['200GETscript.jsapplication/javascript']);
|
||||
await expect(traceViewer.networkRequests).toContainText([/200GETframe.htmltext\/html/]);
|
||||
await expect(traceViewer.networkRequests).toContainText([/aborted.*style.cssx-unknown/]);
|
||||
await expect(traceViewer.networkRequests).toContainText([/200GETscript.jsapplication\/javascript/]);
|
||||
});
|
||||
|
||||
test('should show snapshot URL', async ({ page, runAndTrace, server }) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user