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', Response: 'response',
RequestFailed: 'requestfailed', RequestFailed: 'requestfailed',
RequestFinished: 'requestfinished', RequestFinished: 'requestfinished',
RequestAborted: 'requestaborted',
RequestFulfilled: 'requestfulfilled',
RequestContinued: 'requestcontinued',
BeforeClose: 'beforeclose', BeforeClose: 'beforeclose',
VideoStarted: 'videostarted', 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.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.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.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); 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) { private _storeResponseContent(buffer: Buffer | undefined, content: har.Content, resourceType: string) {
if (!buffer) { if (!buffer) {
content.size = 0; content.size = 0;

View File

@ -25,6 +25,7 @@ import { SdkObject } from './instrumentation';
import type { HeadersArray, NameValue } from '../common/types'; import type { HeadersArray, NameValue } from '../common/types';
import { APIRequestContext } from './fetch'; import { APIRequestContext } from './fetch';
import type { NormalizedContinueOverrides } from './types'; import type { NormalizedContinueOverrides } from './types';
import { BrowserContext } from './browserContext';
export function filterCookies(cookies: channels.NetworkCookie[], urls: string[]): channels.NetworkCookie[] { export function filterCookies(cookies: channels.NetworkCookie[], urls: string[]): channels.NetworkCookie[] {
const parsedURLs = urls.map(s => new URL(s)); const parsedURLs = urls.map(s => new URL(s));
@ -257,6 +258,7 @@ export class Route extends SdkObject {
async abort(errorCode: string = 'failed') { async abort(errorCode: string = 'failed') {
this._startHandling(); this._startHandling();
await this._delegate.abort(errorCode); await this._delegate.abort(errorCode);
this._request._context.emit(BrowserContext.Events.RequestAborted, this._request);
this._endHandling(); this._endHandling();
} }
@ -289,6 +291,7 @@ export class Route extends SdkObject {
body, body,
isBase64, isBase64,
}); });
this._request._context.emit(BrowserContext.Events.RequestFulfilled, this._request);
this._endHandling(); this._endHandling();
} }
@ -320,6 +323,8 @@ export class Route extends SdkObject {
} }
this._request._setOverrides(overrides); this._request._setOverrides(overrides);
await this._delegate.continue(this._request, 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(); this._endHandling();
} }

View File

@ -34,18 +34,21 @@
flex: 1; flex: 1;
} }
.network-request-title-status, .network-request-title-status {
.network-request-title-method { padding: 0 2px;
padding-right: 5px; border-radius: 4px;
margin: 2px;
line-height: 20px;
} }
.network-request-title-status.status-failure { .network-request-title-status.status-failure {
background-color: var(--red); color: var(--vscode-statusBar-foreground);
color: var(--white); background-color: var(--vscode-statusBarItem-errorBackground);
} }
.network-request-title-status.status-neutral { .network-request-title-status.status-route {
background-color: var(--white); color: var(--vscode-statusBar-foreground);
background-color: var(--vscode-statusBar-background);
} }
.network-request-title-url { .network-request-title-url {
@ -54,6 +57,10 @@
flex: 1; flex: 1;
} }
.network-request-title-content-type {
margin-left: 6px;
}
.network-request-details { .network-request-details {
width: 100%; width: 100%;
user-select: text; user-select: text;

View File

@ -18,6 +18,7 @@ import type { ResourceSnapshot } from '@trace/snapshot';
import { Expandable } from '@web/components/expandable'; import { Expandable } from '@web/components/expandable';
import * as React from 'react'; import * as React from 'react';
import './networkResourceDetails.css'; import './networkResourceDetails.css';
import type { Entry } from '@trace/har';
export const NetworkResourceDetails: React.FunctionComponent<{ export const NetworkResourceDetails: React.FunctionComponent<{
resource: ResourceSnapshot, resource: ResourceSnapshot,
@ -64,64 +65,28 @@ export const NetworkResourceDetails: React.FunctionComponent<{
readResources(); readResources();
}, [expanded, resource]); }, [expanded, resource]);
function formatBody(body: string | null, contentType: string): string { const { routeStatus, requestContentType, resourceName, contentType } = React.useMemo(() => {
if (body === null) const routeStatus = formatRouteStatus(resource);
return 'Loading...'; 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; const renderTitle = React.useCallback(() => {
return <div className='network-request-title'>
if (bodyStr === '') {routeStatus && <div className={'network-request-title-status status-route'}>{routeStatus}</div> }
return '<Empty>'; {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>}
if (contentType.includes('application/json')) { <div className='network-request-title-status'>{resource.request.method}</div>
try { <div className='network-request-title-url'>{resourceName}</div>
return JSON.stringify(JSON.parse(bodyStr), null, 2); <div className='network-request-title-content-type'>{contentType}</div>
} catch (err) { </div>;
return bodyStr; }, [contentType, resource, resourceName, routeStatus]);
}
}
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>;
}
};
return <div return <div
className={'network-request ' + (selected ? 'selected' : '')} onClick={() => setSelected(index)}> className={'network-request ' + (selected ? 'selected' : '')} onClick={() => setSelected(index)}>
@ -144,3 +109,43 @@ export const NetworkResourceDetails: React.FunctionComponent<{
</Expandable> </Expandable>
</div>; </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; _monotonicTime?: number;
_serverPort?: number; _serverPort?: number;
_securityDetails?: SecurityDetails; _securityDetails?: SecurityDetails;
_wasAborted?: boolean;
_wasFulfilled?: boolean;
_wasContinued?: boolean;
}; };
export type Request = { export type Request = {

View File

@ -31,7 +31,8 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s
const context = await browser.newContext(); const context = await browser.newContext();
await context.tracing.start({ name: 'test', screenshots: true, snapshots: true, sources: true }); await context.tracing.start({ name: 'test', screenshots: true, snapshots: true, sources: true });
const page = await context.newPage(); 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 page.setContent('<button>Click</button>');
await expect(page.locator('button')).toHaveText('Click'); await expect(page.locator('button')).toHaveText('Click');
await expect(page.getByTestId('amazing-btn')).toBeHidden(); await expect(page.getByTestId('amazing-btn')).toBeHidden();
@ -101,6 +102,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]); const traceViewer = await showTraceViewer([traceFile]);
await expect(traceViewer.actionTitles).toHaveText([ await expect(traceViewer.actionTitles).toHaveText([
/browserContext.newPage/, /browserContext.newPage/,
/page.route/,
/page.gotodata:text\/html,<html>Hello world<\/html>/, /page.gotodata:text\/html,<html>Hello world<\/html>/,
/page.setContent/, /page.setContent/,
/expect.toHaveTextlocator\('button'\)/, /expect.toHaveTextlocator\('button'\)/,
@ -113,6 +115,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
/page.waitForResponse/, /page.waitForResponse/,
/page.waitForTimeout/, /page.waitForTimeout/,
/page.gotohttp:\/\/localhost:\d+\/frames\/frame.html/, /page.gotohttp:\/\/localhost:\d+\/frames\/frame.html/,
/route.abort/,
/page.setViewportSize/, /page.setViewportSize/,
]); ]);
}); });
@ -216,9 +219,9 @@ test('should have network requests', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]); const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.selectAction('http://localhost'); await traceViewer.selectAction('http://localhost');
await traceViewer.showNetworkTab(); await traceViewer.showNetworkTab();
await expect(traceViewer.networkRequests).toContainText(['200GETframe.htmltext/html']); await expect(traceViewer.networkRequests).toContainText([/200GETframe.htmltext\/html/]);
await expect(traceViewer.networkRequests).toContainText(['200GETstyle.csstext/css']); await expect(traceViewer.networkRequests).toContainText([/aborted.*style.cssx-unknown/]);
await expect(traceViewer.networkRequests).toContainText(['200GETscript.jsapplication/javascript']); await expect(traceViewer.networkRequests).toContainText([/200GETscript.jsapplication\/javascript/]);
}); });
test('should show snapshot URL', async ({ page, runAndTrace, server }) => { test('should show snapshot URL', async ({ page, runAndTrace, server }) => {