mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(html): link traces from html report (#9473)
This commit is contained in:
parent
64a3099655
commit
8b1a887756
@ -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>];
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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[] = [];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user