mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 21:58:52 +03:00
chore: migrate tracing ResourceSnapshot to follow har entry format (#8391)
This will ease the migration of tracing to har.
This commit is contained in:
parent
75fb77355a
commit
b0a7843247
@ -96,11 +96,11 @@ export class SnapshotRenderer {
|
||||
|
||||
// First try locating exact resource belonging to this frame.
|
||||
for (const resource of this._resources) {
|
||||
if (resource.timestamp >= snapshot.timestamp)
|
||||
if (resource._monotonicTime >= snapshot.timestamp)
|
||||
break;
|
||||
if (resource.frameId !== snapshot.frameId)
|
||||
if (resource._frameref !== snapshot.frameId)
|
||||
continue;
|
||||
if (resource.url === url) {
|
||||
if (resource.request.url === url) {
|
||||
result = resource;
|
||||
break;
|
||||
}
|
||||
@ -109,9 +109,9 @@ export class SnapshotRenderer {
|
||||
if (!result) {
|
||||
// Then fall back to resource with this URL to account for memory cache.
|
||||
for (const resource of this._resources) {
|
||||
if (resource.timestamp >= snapshot.timestamp)
|
||||
if (resource._monotonicTime >= snapshot.timestamp)
|
||||
break;
|
||||
if (resource.url === url)
|
||||
if (resource.request.url === url)
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
@ -120,7 +120,16 @@ export class SnapshotRenderer {
|
||||
// Patch override if necessary.
|
||||
for (const o of snapshot.resourceOverrides) {
|
||||
if (url === o.url && o.sha1) {
|
||||
result = { ...result, responseSha1: o.sha1 };
|
||||
result = {
|
||||
...result,
|
||||
response: {
|
||||
...result.response,
|
||||
content: {
|
||||
...result.response.content,
|
||||
_sha1: o.sha1,
|
||||
}
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -172,18 +172,21 @@ export class SnapshotServer {
|
||||
if (!resource)
|
||||
return false;
|
||||
|
||||
const sha1 = resource.responseSha1;
|
||||
const sha1 = resource.response.content._sha1;
|
||||
if (!sha1)
|
||||
return false;
|
||||
|
||||
try {
|
||||
const content = this._snapshotStorage.resourceContent(sha1);
|
||||
if (!content)
|
||||
return false;
|
||||
response.statusCode = 200;
|
||||
let contentType = resource.contentType;
|
||||
let contentType = resource.response.content.mimeType;
|
||||
const isTextEncoding = /^text\/|^application\/(javascript|json)/.test(contentType);
|
||||
if (isTextEncoding && !contentType.includes('charset'))
|
||||
contentType = `${contentType}; charset=utf-8`;
|
||||
response.setHeader('Content-Type', contentType);
|
||||
for (const { name, value } of resource.responseHeaders) {
|
||||
for (const { name, value } of resource.response.headers) {
|
||||
try {
|
||||
response.setHeader(name, value.split('\n'));
|
||||
} catch (e) {
|
||||
|
@ -15,18 +15,25 @@
|
||||
*/
|
||||
|
||||
export type ResourceSnapshot = {
|
||||
pageId: string,
|
||||
frameId: string,
|
||||
url: string,
|
||||
type: string,
|
||||
contentType: string,
|
||||
responseHeaders: { name: string, value: string }[],
|
||||
requestHeaders: { name: string, value: string }[],
|
||||
method: string,
|
||||
status: number,
|
||||
requestSha1: string,
|
||||
responseSha1: string,
|
||||
timestamp: number,
|
||||
_frameref: string,
|
||||
request: {
|
||||
url: string,
|
||||
method: string,
|
||||
headers: { name: string, value: string }[],
|
||||
postData?: {
|
||||
text: string,
|
||||
_sha1?: string,
|
||||
},
|
||||
},
|
||||
response: {
|
||||
status: number,
|
||||
headers: { name: string, value: string }[],
|
||||
content: {
|
||||
mimeType: string,
|
||||
_sha1?: string,
|
||||
},
|
||||
},
|
||||
_monotonicTime: number,
|
||||
};
|
||||
|
||||
export type NodeSnapshot =
|
||||
|
@ -205,18 +205,22 @@ export class Snapshotter {
|
||||
}
|
||||
|
||||
const resource: ResourceSnapshot = {
|
||||
pageId: response.frame()._page.guid,
|
||||
frameId: response.frame().guid,
|
||||
url,
|
||||
type: response.request().resourceType(),
|
||||
contentType,
|
||||
responseHeaders: response.headers(),
|
||||
requestHeaders,
|
||||
method,
|
||||
status,
|
||||
requestSha1,
|
||||
responseSha1,
|
||||
timestamp: monotonicTime()
|
||||
_frameref: response.frame().guid,
|
||||
request: {
|
||||
url,
|
||||
method,
|
||||
headers: requestHeaders,
|
||||
postData: requestSha1 ? { text: '', _sha1: requestSha1 } : undefined,
|
||||
},
|
||||
response: {
|
||||
status,
|
||||
headers: response.headers(),
|
||||
content: {
|
||||
mimeType: contentType,
|
||||
_sha1: responseSha1,
|
||||
},
|
||||
},
|
||||
_monotonicTime: monotonicTime()
|
||||
};
|
||||
this._delegate.onResourceSnapshot(resource);
|
||||
}
|
||||
@ -252,6 +256,7 @@ const kMimeToExtension: { [key: string]: string } = {
|
||||
'image/jpeg': 'jpeg',
|
||||
'image/png': 'png',
|
||||
'image/tiff': 'tiff',
|
||||
'image/svg+xml': 'svg',
|
||||
'text/css': 'css',
|
||||
'text/csv': 'csv',
|
||||
'text/html': 'html',
|
||||
|
@ -55,6 +55,8 @@ export type Entry = {
|
||||
timings: Timings;
|
||||
serverIPAddress?: string;
|
||||
connection?: string;
|
||||
_frameref: string;
|
||||
_monotonicTime: number;
|
||||
_serverPort?: number;
|
||||
_securityDetails?: SecurityDetails;
|
||||
};
|
||||
@ -109,6 +111,7 @@ export type PostData = {
|
||||
mimeType: string;
|
||||
params: Param[];
|
||||
text: string;
|
||||
_sha1?: string;
|
||||
};
|
||||
|
||||
export type Param = {
|
||||
@ -124,6 +127,7 @@ export type Content = {
|
||||
mimeType: string;
|
||||
text?: string;
|
||||
encoding?: string;
|
||||
_sha1?: string;
|
||||
};
|
||||
|
||||
export type Cache = {
|
||||
|
@ -22,6 +22,7 @@ import * as network from '../../network';
|
||||
import { Page } from '../../page';
|
||||
import * as har from './har';
|
||||
import * as types from '../../types';
|
||||
import { monotonicTime } from '../../../utils/utils';
|
||||
|
||||
const FALLBACK_HTTP_VERSION = 'HTTP/1.1';
|
||||
|
||||
@ -128,6 +129,8 @@ export class HarTracer {
|
||||
const pageEntry = this._ensurePageEntry(page);
|
||||
const harEntry: har.Entry = {
|
||||
pageref: pageEntry.id,
|
||||
_frameref: request.frame().guid,
|
||||
_monotonicTime: monotonicTime(),
|
||||
startedDateTime: new Date(),
|
||||
time: -1,
|
||||
request: {
|
||||
|
@ -331,7 +331,7 @@ function visitSha1s(object: any, sha1s: Set<string>) {
|
||||
}
|
||||
if (typeof object === 'object') {
|
||||
for (const key in object) {
|
||||
if (key === 'sha1' || key.endsWith('Sha1')) {
|
||||
if (key === 'sha1' || key === '_sha1' || key.endsWith('Sha1')) {
|
||||
const sha1 = object[key];
|
||||
if (sha1)
|
||||
sha1s.add(sha1);
|
||||
|
@ -97,6 +97,7 @@ const extensionToMime: { [key: string]: string } = {
|
||||
'html': 'text/html',
|
||||
'jpeg': 'image/jpeg',
|
||||
'jpg': 'image/jpeg',
|
||||
'gif': 'image/gif',
|
||||
'js': 'application/javascript',
|
||||
'png': 'image/png',
|
||||
'ttf': 'font/ttf',
|
||||
|
@ -91,7 +91,7 @@ export function resourcesForAction(action: ActionTraceEvent): ResourceSnapshot[]
|
||||
|
||||
const nextAction = next(action);
|
||||
result = context(action).resources.filter(resource => {
|
||||
return resource.timestamp > action.metadata.startTime && (!nextAction || resource.timestamp < nextAction.metadata.startTime);
|
||||
return resource._monotonicTime > action.metadata.startTime && (!nextAction || resource._monotonicTime < nextAction.metadata.startTime);
|
||||
});
|
||||
(action as any)[resourcesSymbol] = result;
|
||||
return result;
|
||||
|
@ -36,15 +36,19 @@ export const NetworkResourceDetails: React.FunctionComponent<{
|
||||
|
||||
React.useEffect(() => {
|
||||
const readResources = async () => {
|
||||
if (resource.requestSha1) {
|
||||
const response = await fetch(`/sha1/${resource.requestSha1}`);
|
||||
const requestResource = await response.text();
|
||||
setRequestBody(requestResource);
|
||||
if (resource.request.postData) {
|
||||
if (resource.request.postData._sha1) {
|
||||
const response = await fetch(`/sha1/${resource.request.postData}`);
|
||||
const requestResource = await response.text();
|
||||
setRequestBody(requestResource);
|
||||
} else {
|
||||
setRequestBody(resource.request.postData.text);
|
||||
}
|
||||
}
|
||||
|
||||
if (resource.responseSha1) {
|
||||
const useBase64 = resource.contentType.includes('image');
|
||||
const response = await fetch(`/sha1/${resource.responseSha1}`);
|
||||
if (resource.response.content._sha1) {
|
||||
const useBase64 = resource.response.content.mimeType.includes('image');
|
||||
const response = await fetch(`/sha1/${resource.response.content._sha1}`);
|
||||
if (useBase64) {
|
||||
const blob = await response.blob();
|
||||
const reader = new FileReader();
|
||||
@ -58,7 +62,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{
|
||||
};
|
||||
|
||||
readResources();
|
||||
}, [expanded, resource.responseSha1, resource.requestSha1, resource.contentType]);
|
||||
}, [expanded, resource]);
|
||||
|
||||
function formatBody(body: string | null, contentType: string): string {
|
||||
if (body === null)
|
||||
@ -93,33 +97,33 @@ export const NetworkResourceDetails: React.FunctionComponent<{
|
||||
return 'status-neutral';
|
||||
}
|
||||
|
||||
const requestContentTypeHeader = resource.requestHeaders.find(q => q.name === 'Content-Type');
|
||||
const requestContentTypeHeader = resource.request.headers.find(q => q.name === 'Content-Type');
|
||||
const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : '';
|
||||
const resourceName = resource.url.substring(resource.url.lastIndexOf('/') + 1);
|
||||
const resourceName = resource.request.url.substring(resource.request.url.lastIndexOf('/') + 1);
|
||||
|
||||
return <div
|
||||
className={'network-request ' + (selected ? 'selected' : '')} onClick={() => setSelected(index)}>
|
||||
<Expandable expanded={expanded} setExpanded={setExpanded} style={{ width: '100%' }} title={
|
||||
<div className='network-request-title'>
|
||||
<div className={'network-request-title-status ' + formatStatus(resource.status)}>{resource.status}</div>
|
||||
<div className='network-request-title-method'>{resource.method}</div>
|
||||
<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'>{resource.type}</div>
|
||||
<div className='network-request-title-content-type'>{resource.response.content.mimeType}</div>
|
||||
</div>
|
||||
} body={
|
||||
<div className='network-request-details'>
|
||||
<div className='network-request-details-header'>URL</div>
|
||||
<div className='network-request-details-url'>{resource.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.requestHeaders.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
||||
<div className='network-request-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
||||
<div className='network-request-details-header'>Response Headers</div>
|
||||
<div className='network-request-headers'>{resource.responseHeaders.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
||||
{resource.requestSha1 ? <div className='network-request-details-header'>Request Body</div> : ''}
|
||||
{resource.requestSha1 ? <div className='network-request-body'>{formatBody(requestBody, requestContentType)}</div> : ''}
|
||||
<div className='network-request-headers'>{resource.response.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
||||
{resource.request.postData ? <div className='network-request-details-header'>Request Body</div> : ''}
|
||||
{resource.request.postData ? <div className='network-request-body'>{formatBody(requestBody, requestContentType)}</div> : ''}
|
||||
<div className='network-request-details-header'>Response Body</div>
|
||||
{!resource.responseSha1 ? <div className='network-request-response-body'>Response body is not available for this request.</div> : ''}
|
||||
{!resource.response.content._sha1 ? <div className='network-request-response-body'>Response body is not available for this request.</div> : ''}
|
||||
{responseBody !== null && responseBody.dataUrl ? <img src={responseBody.dataUrl} /> : ''}
|
||||
{responseBody !== null && responseBody.text ? <div className='network-request-response-body'>{formatBody(responseBody.text, resource.contentType)}</div> : ''}
|
||||
{responseBody !== null && responseBody.text ? <div className='network-request-response-body'>{formatBody(responseBody.text, resource.response.content.mimeType)}</div> : ''}
|
||||
</div>
|
||||
}/>
|
||||
</div>;
|
||||
|
@ -146,7 +146,7 @@ it.describe('snapshots', () => {
|
||||
await page.evaluate(() => { (document.styleSheets[0].cssRules[0] as any).style.color = 'blue'; });
|
||||
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
|
||||
const resource = snapshot2.resourceByUrl(`http://localhost:${server.PORT}/style.css`);
|
||||
expect(snapshotter.resourceContent(resource.responseSha1).toString()).toBe('button { color: blue; }');
|
||||
expect(snapshotter.resourceContent(resource.response.content._sha1).toString()).toBe('button { color: blue; }');
|
||||
});
|
||||
|
||||
it('should capture iframe', async ({ page, server, toImpl, browserName, snapshotter, showSnapshot }) => {
|
||||
|
@ -37,8 +37,8 @@ test('should collect trace with resources, but no js', async ({ context, page, s
|
||||
expect(events.find(e => e.metadata?.apiName === 'page.close')).toBeTruthy();
|
||||
|
||||
expect(events.some(e => e.type === 'frame-snapshot')).toBeTruthy();
|
||||
expect(events.some(e => e.type === 'resource-snapshot' && e.snapshot.url.endsWith('style.css'))).toBeTruthy();
|
||||
expect(events.some(e => e.type === 'resource-snapshot' && e.snapshot.url.endsWith('script.js'))).toBeFalsy();
|
||||
expect(events.some(e => e.type === 'resource-snapshot' && e.snapshot.request.url.endsWith('style.css'))).toBeTruthy();
|
||||
expect(events.some(e => e.type === 'resource-snapshot' && e.snapshot.request.url.endsWith('script.js'))).toBeFalsy();
|
||||
expect(events.some(e => e.type === 'screencast-frame')).toBeTruthy();
|
||||
});
|
||||
|
||||
@ -239,7 +239,7 @@ test('should reset and export', async ({ context, page, server }, testInfo) => {
|
||||
expect(trace1.events.find(e => e.metadata?.apiName === 'page.hover')).toBeFalsy();
|
||||
expect(trace1.events.find(e => e.metadata?.apiName === 'page.click' && e.metadata?.error?.error?.message === 'Action was interrupted')).toBeTruthy();
|
||||
expect(trace1.events.some(e => e.type === 'frame-snapshot')).toBeTruthy();
|
||||
expect(trace1.events.some(e => e.type === 'resource-snapshot' && e.snapshot.url.endsWith('style.css'))).toBeTruthy();
|
||||
expect(trace1.events.some(e => e.type === 'resource-snapshot' && e.snapshot.request.url.endsWith('style.css'))).toBeTruthy();
|
||||
|
||||
const trace2 = await parseTrace(testInfo.outputPath('trace2.zip'));
|
||||
expect(trace2.events[0].type).toBe('context-options');
|
||||
|
Loading…
Reference in New Issue
Block a user