Pavel Feldman 2023-06-02 13:00:27 -07:00 committed by GitHub
parent 3c2a8fa306
commit 5a14619bab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 117 additions and 69 deletions

View File

@ -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',
};

View File

@ -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;

View File

@ -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();
}

View File

@ -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;

View File

@ -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 '';
}

View File

@ -68,6 +68,9 @@ export type Entry = {
_monotonicTime?: number;
_serverPort?: number;
_securityDetails?: SecurityDetails;
_wasAborted?: boolean;
_wasFulfilled?: boolean;
_wasContinued?: boolean;
};
export type Request = {

View File

@ -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 }) => {