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',
|
Response: 'response',
|
||||||
RequestFailed: 'requestfailed',
|
RequestFailed: 'requestfailed',
|
||||||
RequestFinished: 'requestfinished',
|
RequestFinished: 'requestfinished',
|
||||||
|
RequestAborted: 'requestaborted',
|
||||||
|
RequestFulfilled: 'requestfulfilled',
|
||||||
|
RequestContinued: 'requestcontinued',
|
||||||
BeforeClose: 'beforeclose',
|
BeforeClose: 'beforeclose',
|
||||||
VideoStarted: 'videostarted',
|
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.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;
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 '';
|
||||||
|
}
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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 }) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user