2021-12-12 14:56:12 -08:00
|
|
|
/*
|
|
|
|
Copyright (c) Microsoft Corporation.
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2022-03-28 17:21:19 -08:00
|
|
|
import type { TestAttachment, TestCase, TestResult, TestStep } from '@playwright-test/reporters/html';
|
2021-12-12 14:56:12 -08:00
|
|
|
import ansi2html from 'ansi-to-html';
|
|
|
|
import * as React from 'react';
|
|
|
|
import { TreeItem } from './treeItem';
|
2021-12-13 16:38:26 -08:00
|
|
|
import { msToString } from './uiUtils';
|
2021-12-14 19:25:07 -08:00
|
|
|
import { AutoChip } from './chip';
|
2021-12-12 14:56:12 -08:00
|
|
|
import { traceImage } from './images';
|
|
|
|
import { AttachmentLink } from './links';
|
|
|
|
import { statusIcon } from './statusIcon';
|
2022-04-01 11:53:56 -08:00
|
|
|
import { ImageDiff, ImageDiffView } from './imageDiffView';
|
2021-12-12 14:56:12 -08:00
|
|
|
import './testResultView.css';
|
|
|
|
|
2022-03-30 16:42:08 -08:00
|
|
|
function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiff[] {
|
|
|
|
const snapshotNameToImageDiff = new Map<string, ImageDiff>();
|
|
|
|
for (const attachment of screenshots) {
|
|
|
|
const match = attachment.name.match(/^(.*)-(expected|actual|diff|previous)(\.[^.]+)?$/);
|
2022-03-11 09:46:13 -07:00
|
|
|
if (!match)
|
|
|
|
continue;
|
|
|
|
const [, name, category, extension = ''] = match;
|
|
|
|
const snapshotName = name + extension;
|
2022-03-30 16:42:08 -08:00
|
|
|
let imageDiff = snapshotNameToImageDiff.get(snapshotName);
|
|
|
|
if (!imageDiff) {
|
|
|
|
imageDiff = { name: snapshotName };
|
|
|
|
snapshotNameToImageDiff.set(snapshotName, imageDiff);
|
2022-03-11 09:46:13 -07:00
|
|
|
}
|
2022-03-30 16:42:08 -08:00
|
|
|
if (category === 'actual')
|
2022-04-01 17:11:15 -08:00
|
|
|
imageDiff.actual = { attachment };
|
2022-03-30 16:42:08 -08:00
|
|
|
if (category === 'expected')
|
2022-04-01 17:11:15 -08:00
|
|
|
imageDiff.expected = { attachment, title: 'Expected' };
|
2022-03-30 16:42:08 -08:00
|
|
|
if (category === 'previous')
|
2022-04-01 17:11:15 -08:00
|
|
|
imageDiff.expected = { attachment, title: 'Previous' };
|
2022-03-30 16:42:08 -08:00
|
|
|
if (category === 'diff')
|
2022-04-01 17:11:15 -08:00
|
|
|
imageDiff.diff = { attachment };
|
2022-03-11 09:46:13 -07:00
|
|
|
}
|
2022-03-30 16:42:08 -08:00
|
|
|
for (const [name, diff] of snapshotNameToImageDiff) {
|
2022-04-01 17:11:15 -08:00
|
|
|
if (!diff.actual || !diff.expected) {
|
2022-03-30 16:42:08 -08:00
|
|
|
snapshotNameToImageDiff.delete(name);
|
|
|
|
} else {
|
2022-04-01 17:11:15 -08:00
|
|
|
screenshots.delete(diff.actual.attachment);
|
|
|
|
screenshots.delete(diff.expected.attachment);
|
2022-03-30 16:42:08 -08:00
|
|
|
screenshots.delete(diff.diff?.attachment!);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return [...snapshotNameToImageDiff.values()];
|
2022-03-11 09:46:13 -07:00
|
|
|
}
|
2021-12-15 19:19:43 -08:00
|
|
|
|
2021-12-12 14:56:12 -08:00
|
|
|
export const TestResultView: React.FC<{
|
|
|
|
test: TestCase,
|
|
|
|
result: TestResult,
|
|
|
|
}> = ({ result }) => {
|
|
|
|
|
2022-03-11 09:46:13 -07:00
|
|
|
const { screenshots, videos, traces, otherAttachments, diffs } = React.useMemo(() => {
|
2022-03-30 16:42:08 -08:00
|
|
|
const attachments = result?.attachments || [];
|
|
|
|
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
|
|
|
|
const videos = attachments.filter(a => a.name === 'video');
|
|
|
|
const traces = attachments.filter(a => a.name === 'trace');
|
|
|
|
const otherAttachments = new Set<TestAttachment>(attachments);
|
|
|
|
[...screenshots, ...videos, ...traces].forEach(a => otherAttachments.delete(a));
|
|
|
|
const diffs = groupImageDiffs(screenshots);
|
|
|
|
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs };
|
2021-12-12 14:56:12 -08:00
|
|
|
}, [ result ]);
|
|
|
|
|
|
|
|
return <div className='test-result'>
|
2022-02-02 19:33:51 -07:00
|
|
|
{!!result.errors.length && <AutoChip header='Errors'>
|
|
|
|
{result.errors.map((error, index) => <ErrorMessage key={'test-result-error-message-' + index} error={error}></ErrorMessage>)}
|
2021-12-14 19:25:07 -08:00
|
|
|
</AutoChip>}
|
|
|
|
{!!result.steps.length && <AutoChip header='Test Steps'>
|
2021-12-12 14:56:12 -08:00
|
|
|
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} depth={0}></StepTreeItem>)}
|
2021-12-14 19:25:07 -08:00
|
|
|
</AutoChip>}
|
2021-12-12 14:56:12 -08:00
|
|
|
|
2022-03-30 16:42:08 -08:00
|
|
|
{diffs.map((diff, index) =>
|
|
|
|
<AutoChip key={`diff-${index}`} header={`Image mismatch: ${diff.name}`}>
|
|
|
|
<ImageDiffView key='image-diff' imageDiff={diff}></ImageDiffView>
|
2022-03-11 09:46:13 -07:00
|
|
|
</AutoChip>
|
|
|
|
)}
|
2021-12-12 14:56:12 -08:00
|
|
|
|
2021-12-14 19:25:07 -08:00
|
|
|
{!!screenshots.length && <AutoChip header='Screenshots'>
|
2021-12-12 14:56:12 -08:00
|
|
|
{screenshots.map((a, i) => {
|
|
|
|
return <div key={`screenshot-${i}`}>
|
|
|
|
<img src={a.path} />
|
|
|
|
<AttachmentLink attachment={a}></AttachmentLink>
|
|
|
|
</div>;
|
|
|
|
})}
|
2021-12-14 19:25:07 -08:00
|
|
|
</AutoChip>}
|
2021-12-12 14:56:12 -08:00
|
|
|
|
2021-12-14 19:25:07 -08:00
|
|
|
{!!traces.length && <AutoChip header='Traces'>
|
2022-02-16 09:09:42 -08:00
|
|
|
{<div>
|
|
|
|
<a href={`trace/index.html?${traces.map((a, i) => `trace=${new URL(a.path!, window.location.href)}`).join('&')}`}>
|
2021-12-12 14:56:12 -08:00
|
|
|
<img src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
|
|
|
|
</a>
|
2022-02-16 09:09:42 -08:00
|
|
|
{traces.map((a, i) => <AttachmentLink key={`trace-${i}`} attachment={a} linkName={traces.length === 1 ? 'trace' : `trace-${i + 1}`}></AttachmentLink>)}
|
|
|
|
</div>}
|
2021-12-14 19:25:07 -08:00
|
|
|
</AutoChip>}
|
2021-12-12 14:56:12 -08:00
|
|
|
|
2021-12-14 19:25:07 -08:00
|
|
|
{!!videos.length && <AutoChip header='Videos'>
|
2021-12-12 14:56:12 -08:00
|
|
|
{videos.map((a, i) => <div key={`video-${i}`}>
|
|
|
|
<video controls>
|
|
|
|
<source src={a.path} type={a.contentType}/>
|
|
|
|
</video>
|
|
|
|
<AttachmentLink attachment={a}></AttachmentLink>
|
|
|
|
</div>)}
|
2021-12-14 19:25:07 -08:00
|
|
|
</AutoChip>}
|
2021-12-12 14:56:12 -08:00
|
|
|
|
2021-12-15 19:19:43 -08:00
|
|
|
{!!otherAttachments.size && <AutoChip header='Attachments'>
|
|
|
|
{[...otherAttachments].map((a, i) => <AttachmentLink key={`attachment-link-${i}`} attachment={a}></AttachmentLink>)}
|
2021-12-14 19:25:07 -08:00
|
|
|
</AutoChip>}
|
2021-12-12 14:56:12 -08:00
|
|
|
</div>;
|
|
|
|
};
|
|
|
|
|
|
|
|
const StepTreeItem: React.FC<{
|
|
|
|
step: TestStep;
|
|
|
|
depth: number,
|
|
|
|
}> = ({ step, depth }) => {
|
|
|
|
return <TreeItem title={<span>
|
|
|
|
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
|
|
|
|
{statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')}
|
|
|
|
<span>{step.title}</span>
|
2022-01-03 21:17:17 -08:00
|
|
|
{step.count > 1 && <> ✕ <span className='test-result-counter'>{step.count}</span></>}
|
2021-12-12 14:56:12 -08:00
|
|
|
{step.location && <span className='test-result-path'>— {step.location.file}:{step.location.line}</span>}
|
|
|
|
</span>} loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => {
|
|
|
|
const children = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1}></StepTreeItem>);
|
|
|
|
if (step.snippet)
|
|
|
|
children.unshift(<ErrorMessage key='line' error={step.snippet}></ErrorMessage>);
|
|
|
|
return children;
|
|
|
|
} : undefined} depth={depth}></TreeItem>;
|
|
|
|
};
|
|
|
|
|
|
|
|
const ErrorMessage: React.FC<{
|
|
|
|
error: string;
|
|
|
|
}> = ({ error }) => {
|
|
|
|
const html = React.useMemo(() => {
|
|
|
|
const config: any = {
|
|
|
|
bg: 'var(--color-canvas-subtle)',
|
|
|
|
fg: 'var(--color-fg-default)',
|
|
|
|
};
|
|
|
|
config.colors = ansiColors;
|
|
|
|
return new ansi2html(config).toHtml(escapeHTML(error));
|
|
|
|
}, [error]);
|
|
|
|
return <div className='test-result-error-message' dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
|
|
|
};
|
|
|
|
|
|
|
|
const ansiColors = {
|
|
|
|
0: '#000',
|
|
|
|
1: '#C00',
|
|
|
|
2: '#0C0',
|
|
|
|
3: '#C50',
|
|
|
|
4: '#00C',
|
|
|
|
5: '#C0C',
|
|
|
|
6: '#0CC',
|
|
|
|
7: '#CCC',
|
|
|
|
8: '#555',
|
|
|
|
9: '#F55',
|
|
|
|
10: '#5F5',
|
|
|
|
11: '#FF5',
|
|
|
|
12: '#55F',
|
|
|
|
13: '#F5F',
|
|
|
|
14: '#5FF',
|
|
|
|
15: '#FFF'
|
|
|
|
};
|
|
|
|
|
|
|
|
function escapeHTML(text: string): string {
|
|
|
|
return text.replace(/[&"<>]/g, c => ({ '&': '&', '"': '"', '<': '<', '>': '>' }[c]!));
|
|
|
|
}
|