feat(html): link traces from html report (#9473)

This commit is contained in:
Pavel Feldman 2021-10-13 10:07:29 -08:00 committed by GitHub
parent 64a3099655
commit 8b1a887756
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 70 additions and 42 deletions

View File

@ -163,27 +163,28 @@ const TestResultView: React.FC<{
result: TestResult,
}> = ({ result }) => {
const { screenshots, videos, otherAttachments, attachmentsMap } = React.useMemo(() => {
const { screenshots, videos, traces, otherAttachments, attachmentsMap } = React.useMemo(() => {
const attachmentsMap = new Map<string, TestAttachment>();
const attachments = result?.attachments || [];
const otherAttachments: TestAttachment[] = [];
const screenshots = attachments.filter(a => a.name === 'screenshot');
const videos = attachments.filter(a => a.name === 'video');
const knownNames = new Set(['screenshot', 'image', 'expected', 'actual', 'diff', 'video']);
const traces = attachments.filter(a => a.name === 'trace');
const knownNames = new Set(['screenshot', 'image', 'expected', 'actual', 'diff', 'video', 'trace']);
for (const a of attachments) {
attachmentsMap.set(a.name, a);
if (!knownNames.has(a.name))
otherAttachments.push(a);
}
return { attachmentsMap, screenshots, videos, otherAttachments };
return { attachmentsMap, screenshots, videos, otherAttachments, traces };
}, [ result ]);
const expected = attachmentsMap.get('expected');
const actual = attachmentsMap.get('actual');
const diff = attachmentsMap.get('diff');
return <div className='test-result'>
{result.error && <ErrorMessage key={-1} error={result.error}></ErrorMessage>}
{result.steps.map((step, i) => <StepTreeItem key={i} step={step} depth={0}></StepTreeItem>)}
{result.error && <ErrorMessage key='error-message' error={result.error}></ErrorMessage>}
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} depth={0}></StepTreeItem>)}
{expected && actual && <div className='vbox'>
<ImageDiff actual={actual} expected={expected} diff={diff}></ImageDiff>
@ -192,24 +193,29 @@ const TestResultView: React.FC<{
{diff && <AttachmentLink key={`diff`} attachment={diff}></AttachmentLink>}
</div>}
{!!screenshots.length && <div className='test-overview-title'>Screenshots</div>}
{!!screenshots.length && <div key='screenshots-title' className='test-overview-title'>Screenshots</div>}
{screenshots.map((a, i) => {
return <div className='vbox'>
<img key={`screenshot-${i}`} src={a.path} />
<AttachmentLink key={`screenshot-link-${i}`} attachment={a}></AttachmentLink>
return <div key={`screenshot-${i}`} className='vbox'>
<img src={a.path} />
<AttachmentLink attachment={a}></AttachmentLink>
</div>;
})}
{!!videos.length && <div className='test-overview-title'>Videos</div>}
{videos.map((a, i) => <div className='vbox'>
<video key={`video-${i}`} controls>
<source src={a.path} type={a.contentType}/>
</video>
<AttachmentLink key={`video-link-${i}`} attachment={a}></AttachmentLink>
{!!traces.length && <div key='traces-title' className='test-overview-title'>Traces</div>}
{traces.map((a, i) => <div key={`trace-${i}`} className='vbox'>
<AttachmentLink attachment={a} href={`trace/index.html?trace=${window.location.origin}/` + a.path}></AttachmentLink>
</div>)}
{!!otherAttachments && <div className='test-overview-title'>Attachments</div>}
{otherAttachments.map((a, i) => <AttachmentLink key={`attachment-${i}`} attachment={a}></AttachmentLink>)}
{!!videos.length && <div key='videos-title' className='test-overview-title'>Videos</div>}
{videos.map((a, i) => <div key={`video-${i}`} className='vbox'>
<video controls>
<source src={a.path} type={a.contentType}/>
</video>
<AttachmentLink attachment={a}></AttachmentLink>
</div>)}
{!!otherAttachments && <div key='attachments-title' className='test-overview-title'>Attachments</div>}
{otherAttachments.map((a, i) => <AttachmentLink key={`attachment-link-${i}`} attachment={a}></AttachmentLink>)}
</div>;
};
@ -243,10 +249,11 @@ const StatsView: React.FC<{
export const AttachmentLink: React.FunctionComponent<{
attachment: TestAttachment,
}> = ({ attachment }) => {
href?: string,
}> = ({ attachment, href }) => {
return <TreeItem title={<div style={{ display: 'flex', alignItems: 'center', flex: 'auto' }}>
<span className={'codicon codicon-cloud-download'}></span>
{attachment.path && <a href={attachment.path} target='_blank'>{attachment.name}</a>}
{attachment.path && <a href={href || attachment.path} target='_blank'>{attachment.name}</a>}
{attachment.body && <span>{attachment.name}</span>}
</div>} loadChildren={attachment.body ? () => {
return [<div className='attachment-body'>${attachment.body}</div>];

View File

@ -185,7 +185,7 @@ function snapshotScript() {
iframe.setAttribute('src', 'data:text/html,<body style="background: #ddd"></body>');
} else {
// Append query parameters to inherit ?name= or ?time= values from parent.
iframe.setAttribute('src', window.location.origin + src + window.location.search);
iframe.setAttribute('src', new URL(src + window.location.search, window.location.href).toString());
}
}

View File

@ -20,7 +20,9 @@ import { TraceModel } from './traceModel';
// @ts-ignore
declare const self: ServiceWorkerGlobalScope;
self.addEventListener('install', function(event: any) {});
self.addEventListener('install', function(event: any) {
self.skipWaiting();
});
self.addEventListener('activate', function(event: any) {
event.waitUntil(self.clients.claim());
@ -28,6 +30,7 @@ self.addEventListener('activate', function(event: any) {
let traceModel: TraceModel | undefined;
let snapshotServer: SnapshotServer | undefined;
const scopePath = new URL(self.registration.scope).pathname;
async function loadTrace(trace: string): Promise<TraceModel> {
const traceModel = new TraceModel();
@ -39,13 +42,14 @@ async function loadTrace(trace: string): Promise<TraceModel> {
// @ts-ignore
async function doFetch(event: FetchEvent): Promise<Response> {
const request = event.request;
const { pathname, searchParams } = new URL(request.url);
const url = new URL(request.url);
const snapshotUrl = request.mode === 'navigate' ?
request.url : (await self.clients.get(event.clientId))!.url;
if (request.url.startsWith(self.location.origin)) {
if (pathname === '/context') {
const trace = searchParams.get('trace')!;
if (request.url.startsWith(self.registration.scope)) {
const relativePath = url.pathname.substring(scopePath.length - 1);
if (relativePath === '/context') {
const trace = url.searchParams.get('trace')!;
traceModel = await loadTrace(trace);
snapshotServer = new SnapshotServer(traceModel.storage());
return new Response(JSON.stringify(traceModel!.contextEntry), {
@ -53,12 +57,12 @@ async function doFetch(event: FetchEvent): Promise<Response> {
headers: { 'Content-Type': 'application/json' }
});
}
if (pathname.startsWith('/snapshotSize/'))
return snapshotServer!.serveSnapshotSize(pathname, searchParams);
if (pathname.startsWith('/snapshot/'))
return snapshotServer!.serveSnapshot(pathname, searchParams, snapshotUrl);
if (pathname.startsWith('/sha1/')) {
const blob = await traceModel!.resourceForSha1(pathname.slice('/sha1/'.length));
if (relativePath.startsWith('/snapshotSize/'))
return snapshotServer!.serveSnapshotSize(relativePath, url.searchParams);
if (relativePath.startsWith('/snapshot/'))
return snapshotServer!.serveSnapshot(relativePath, url.searchParams, snapshotUrl);
if (relativePath.startsWith('/sha1/')) {
const blob = await traceModel!.resourceForSha1(relativePath.slice('/sha1/'.length));
if (blob)
return new Response(blob, { status: 200 });
else
@ -67,6 +71,7 @@ async function doFetch(event: FetchEvent): Promise<Response> {
return fetch(event.request);
}
if (!snapshotServer)
return new Response(null, { status: 404 });
return snapshotServer!.serveResource(request.url, snapshotUrl);

View File

@ -61,7 +61,7 @@ export const FilmStrip: React.FunctionComponent<{
top: measure.bottom + 5,
left: Math.min(previewPoint!.x, measure.width - previewSize.width - 10),
}}>
<img src={`/sha1/${previewImage.sha1}`} width={previewSize.width} height={previewSize.height} />
<img src={`sha1/${previewImage.sha1}`} width={previewSize.width} height={previewSize.height} />
</div>
}
</div>;
@ -97,7 +97,7 @@ const FilmStripLane: React.FunctionComponent<{
frames.push(<div className='film-strip-frame' key={i} style={{
width: frameSize.width,
height: frameSize.height,
backgroundImage: `url(/sha1/${screencastFrames[index].sha1})`,
backgroundImage: `url(sha1/${screencastFrames[index].sha1})`,
backgroundSize: `${frameSize.width}px ${frameSize.height}px`,
margin: frameMargin,
marginRight: frameMargin,
@ -107,7 +107,7 @@ const FilmStripLane: React.FunctionComponent<{
frames.push(<div className='film-strip-frame' key={i} style={{
width: frameSize.width,
height: frameSize.height,
backgroundImage: `url(/sha1/${screencastFrames[screencastFrames.length - 1].sha1})`,
backgroundImage: `url(sha1/${screencastFrames[screencastFrames.length - 1].sha1})`,
backgroundSize: `${frameSize.width}px ${frameSize.height}px`,
margin: frameMargin,
marginRight: frameMargin,

View File

@ -38,7 +38,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{
const readResources = async () => {
if (resource.request.postData) {
if (resource.request.postData._sha1) {
const response = await fetch(`/sha1/${resource.request.postData._sha1}`);
const response = await fetch(`sha1/${resource.request.postData._sha1}`);
const requestResource = await response.text();
setRequestBody(requestResource);
} else {
@ -48,7 +48,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{
if (resource.response.content._sha1) {
const useBase64 = resource.response.content.mimeType.includes('image');
const response = await fetch(`/sha1/${resource.response.content._sha1}`);
const response = await fetch(`sha1/${resource.response.content._sha1}`);
if (useBase64) {
const blob = await response.blob();
const reader = new FileReader();

View File

@ -41,8 +41,8 @@ export const SnapshotTab: React.FunctionComponent<{
if (action) {
const snapshot = snapshots[snapshotIndex];
if (snapshot && snapshot.snapshotName) {
snapshotUrl = `${window.location.origin}/snapshot/${action.metadata.pageId}?name=${snapshot.snapshotName}`;
snapshotSizeUrl = `${window.location.origin}/snapshotSize/${action.metadata.pageId}?name=${snapshot.snapshotName}`;
snapshotUrl = new URL(`snapshot/${action.metadata.pageId}?name=${snapshot.snapshotName}`, window.location.href).toString();
snapshotSizeUrl = new URL(`snapshotSize/${action.metadata.pageId}?name=${snapshot.snapshotName}`, window.location.href).toString();
if (snapshot.snapshotName.includes('action')) {
pointX = action.metadata.point?.x;
pointY = action.metadata.point?.y;

View File

@ -39,9 +39,13 @@ export const Workbench: React.FunctionComponent<{
React.useEffect(() => {
(async () => {
const contextEntry = (await fetch(`/context?trace=${traceURL}`).then(response => response.json())) as ContextEntry;
modelUtil.indexModel(contextEntry);
setContextEntry(contextEntry);
if (traceURL) {
const contextEntry = (await fetch(`context?trace=${traceURL}`).then(response => response.json())) as ContextEntry;
modelUtil.indexModel(contextEntry);
setContextEntry(contextEntry);
} else {
setContextEntry(emptyContext);
}
})();
}, [traceURL]);

View File

@ -121,7 +121,7 @@ class HtmlReporter {
if (!process.env.CI && !process.env.PWTEST_SKIP_TEST_OUTPUT) {
const server = new HttpServer();
server.routePrefix('/', (request, response) => {
let relativePath = request.url!;
let relativePath = new URL('http://localhost' + request.url).pathname;
if (relativePath === '/')
relativePath = '/index.html';
const absolutePath = path.join(reportFolder, ...relativePath.split('/'));
@ -149,10 +149,22 @@ class HtmlBuilder {
this._reportFolder = path.resolve(process.cwd(), outputDir);
this._dataFolder = path.join(this._reportFolder, 'data');
fs.mkdirSync(this._dataFolder, { recursive: true });
// Copy app.
const appFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'web', 'htmlReport');
for (const file of fs.readdirSync(appFolder))
fs.copyFileSync(path.join(appFolder, file), path.join(this._reportFolder, file));
// Copy trace viewer.
const traceViewerFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'web', 'traceViewer');
const traceViewerTargetFolder = path.join(this._reportFolder, 'trace');
fs.mkdirSync(traceViewerTargetFolder, { recursive: true });
// TODO (#9471): remove file filter when the babel build is fixed.
for (const file of fs.readdirSync(traceViewerFolder)) {
if (fs.statSync(path.join(traceViewerFolder, file)).isFile())
fs.copyFileSync(path.join(traceViewerFolder, file), path.join(traceViewerTargetFolder, file));
}
const projects: ProjectTreeItem[] = [];
for (const projectJson of rawReports) {
const suites: SuiteTreeItem[] = [];