chore(html): only copy trace viewer for reports with traces (#9579)

This commit is contained in:
Pavel Feldman 2021-10-18 07:03:04 -08:00 committed by GitHub
parent 4364c5f248
commit 432fb453e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 124 additions and 37 deletions

View File

@ -26,12 +26,18 @@ export const TreeItem: React.FunctionComponent<{
}> = ({ title, loadChildren, onClick, expandByDefault, depth, selected }) => {
const [expanded, setExpanded] = React.useState(expandByDefault || false);
const className = selected ? 'tree-item-title selected' : 'tree-item-title';
return <div style={{ display: 'flex', flexDirection: 'column', width: '100%', flex: 'none' }}>
<div className={className} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', whiteSpace: 'nowrap', paddingLeft: depth * 16 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
<div className={'codicon codicon-' + (expanded ? 'chevron-down' : 'chevron-right')}
style={{ cursor: 'pointer', color: 'var(--color)', visibility: loadChildren ? 'visible' : 'hidden' }} />
return <div className={'tree-item'}>
<span className={className} style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
<span className={'codicon codicon-' + (expanded ? 'chevron-down' : 'chevron-right')}
style={{
cursor: 'pointer',
color: 'var(--color)',
visibility: loadChildren ? 'visible' : 'hidden',
position: 'relative',
top: 3
}} />
{title}
</div>
</span>
{expanded && loadChildren?.()}
</div>;
};

View File

@ -34,6 +34,12 @@ body {
overflow: auto;
}
.global-stats {
padding-left: 34px;
margin-top: 20px;
font-weight: 600;
}
.suite-tree-column {
line-height: 18px;
flex: auto;
@ -47,11 +53,21 @@ body {
margin: 20px;
}
.tree-item {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.tree-item-title {
padding: 8px 8px 8px 0;
cursor: pointer;
}
.chip-body .tree-item {
max-width: 600px;
line-height: 38px;
}
.tree-item-body {
min-height: 18px;
}
@ -159,6 +175,7 @@ body {
}
.test-case-column .tab-strip {
margin-top: 10px;
background-color: inherit;
}
@ -223,18 +240,21 @@ body {
video, img {
flex: none;
box-shadow: var(--box-shadow-thick);
width: 80%;
margin: 20px auto;
min-width: 80%;
min-height: 300px;
min-width: 200px;
max-width: 80%;
}
.flow-container {
max-width: 1280px;
max-width: 1024px;
margin: 0 auto;
width: 100%;
}
.file-summary-list {
padding-bottom: 20px;
}
.file-summary-list .chip-body a:not(:nth-child(1)) .test-summary,
.failed-test:not(:nth-child(1)) {
border-top: 1px solid var(--color-border-default);
@ -274,16 +294,18 @@ a.no-decorations {
}
.chip-header {
display: flex;
align-items: center;
border: 1px solid var(--color-border-default);
border-top-left-radius: 6px;
border-top-right-radius: 6px;
background-color: var(--color-page-header-bg);
padding: 10px;
padding: 0 10px;
border-bottom: none;
margin-top: 20px;
font-weight: 600;
line-height: 38px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chip-header.expanded-false {
@ -296,6 +318,11 @@ a.no-decorations {
cursor: pointer;
}
.chip-header .codicon {
position: relative;
top: 3px;
}
.chip-body {
border: 1px solid var(--color-border-default);
border-bottom-left-radius: 6px;
@ -331,6 +358,10 @@ a.no-decorations {
color: var(--color-fg-muted);
}
.test-summary.outcome-skipped {
color: var(--color-fg-muted);
}
.octicon {
display: inline-block;
overflow: visible !important;
@ -354,6 +385,10 @@ a.no-decorations {
color: var(--color-fg-muted) !important;
}
.color-fg-white {
color: white !important;
}
.octicon {
margin-right: 7px;
flex: none;

View File

@ -67,6 +67,10 @@ const AllTestFilesSummaryView: React.FC<{
setFileExpanded: (fileId: string, expanded: boolean) => void;
}> = ({ report, isFileExpanded, setFileExpanded }) => {
return <div className='file-summary-list'>
{report && <div className='global-stats'>
<span>Ran {report.stats.total} tests</span>
<StatsView stats={report.stats}></StatsView>
</div>}
{report && (report.files || []).map((file, i) => <TestFileSummaryView key={`file-${i}`} report={report} file={file} isFileExpanded={isFileExpanded} setFileExpanded={setFileExpanded}></TestFileSummaryView>)}
</div>;
};
@ -77,13 +81,21 @@ const TestFileSummaryView: React.FC<{
isFileExpanded: (fileId: string) => boolean;
setFileExpanded: (fileId: string, expanded: boolean) => void;
}> = ({ file, report, isFileExpanded, setFileExpanded }) => {
return <Chip expanded={isFileExpanded(file.fileId)} setExpanded={(expanded => setFileExpanded(file.fileId, expanded))} header={<span>{file.fileName}<StatsView stats={file.stats}></StatsView></span>}>
return <Chip
expanded={isFileExpanded(file.fileId)}
setExpanded={(expanded => setFileExpanded(file.fileId, expanded))}
header={<span>
{file.fileName}
<StatsView stats={file.stats}></StatsView>
<span style={{ float: 'right' }}>{msToString(file.stats.duration)}</span>
</span>}>
{file.tests.map((test, i) => <Link key={`test-${i}`} href={`/?testId=${test.testId}`}>
<div className='test-summary'>
<span style={{ float: 'right', marginTop: 10 }} className={'label label-color-' + (report.projectNames.indexOf(test.projectName) % 8)}>{test.projectName}</span>
<div className={'test-summary outcome-' + test.outcome}>
{statusIcon(test.outcome)}
{test.title}
<span className='test-summary-path'> {test.path.join(' ')}</span>
{report.projectNames.length > 1 && !!test.projectName && <span className={'label label-color-' + (report.projectNames.indexOf(test.projectName) % 8)}>{test.projectName}</span>}
<span style={{ float: 'right' }}>{msToString(test.duration)}</span>
</div>
</Link>)}
</Chip>;
@ -116,7 +128,7 @@ const TestCaseView: React.FC<{
return <div className='test-case-column vbox'>
{test && <div className='test-case-title'>{test?.title}</div>}
{test && <div className='test-case-location'>{test.path.join(' ')}</div>}
{test && <div><span className={'label label-color-' + (report.projectNames.indexOf(test.projectName) % 8)}>{test.projectName}</span></div>}
{test && !!test.projectName && <div><span className={'label label-color-' + (report.projectNames.indexOf(test.projectName) % 8)}>{test.projectName}</span></div>}
{test && <TabbedPane tabs={
test.results.map((result, index) => ({
id: String(index),
@ -201,12 +213,11 @@ const StepTreeItem: React.FC<{
step: TestStep;
depth: number,
}> = ({ step, depth }) => {
return <TreeItem title={<div style={{ display: 'flex', alignItems: 'center', flex: 'auto' }}>
return <TreeItem title={<span>
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
{statusIcon(step.error ? 'failed' : 'passed')}
<span style={{ whiteSpace: 'pre' }}>{step.title}</span>
<div style={{ flex: 'auto' }}></div>
<div>{msToString(step.duration)}</div>
</div>} loadChildren={step.steps.length + (step.error ? 1 : 0) ? () => {
<span>{step.title}</span>
</span>} loadChildren={step.steps.length + (step.error ? 1 : 0) ? () => {
const children = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1}></StepTreeItem>);
if (step.error)
children.unshift(<ErrorMessage key={-1} error={step.error} mode='light'></ErrorMessage>);
@ -287,7 +298,7 @@ function statusIcon(status: 'failed' | 'timedOut' | 'skipped' | 'passed' | 'expe
<path fill-rule='evenodd' d='M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z'></path>
</svg>;
case 'skipped':
return <svg className='octicon color-fg-muted' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'>
return <svg className='octicon color-fg-white' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'>
<path fill-rule='evenodd' d='M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm3.28 5.78a.75.75 0 00-1.06-1.06l-5.5 5.5a.75.75 0 101.06 1.06l5.5-5.5z'></path>
</svg>;
}

View File

@ -43,6 +43,7 @@ export type Location = {
export type HTMLReport = {
files: TestFileSummary[];
stats: Stats;
testIdToFileId: { [key: string]: string };
projectNames: string[];
};
@ -182,6 +183,7 @@ class HtmlBuilder {
private _testPath = new Map<string, string[]>();
private _rootDir: string;
private _dataFolder: string;
private _hasTraces = false;
constructor(outputDir: string, rootDir: string) {
this._rootDir = rootDir;
@ -192,18 +194,6 @@ class HtmlBuilder {
build(rawReports: JsonReport[]): boolean {
fs.mkdirSync(this._dataFolder, { recursive: true });
// Copy app.
const appFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'webpack', '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', 'webpack', 'traceViewer');
const traceViewerTargetFolder = path.join(this._reportFolder, 'trace');
fs.mkdirSync(traceViewerTargetFolder, { recursive: true });
for (const file of fs.readdirSync(traceViewerFolder))
fs.copyFileSync(path.join(traceViewerFolder, file), path.join(traceViewerTargetFolder, file));
const data = new Map<string, { testFile: TestFile, testFileSummary: TestFileSummary }>();
for (const projectJson of rawReports) {
for (const file of projectJson.suites) {
@ -261,14 +251,37 @@ class HtmlBuilder {
const htmlReport: HTMLReport = {
files: [...data.values()].map(e => e.testFileSummary),
testIdToFileId,
projectNames: rawReports.map(r => r.project.name)
projectNames: rawReports.map(r => r.project.name),
stats: [...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats())
};
htmlReport.files.sort((f1, f2) => {
const w1 = f1.stats.unexpected * 1000 + f1.stats.flaky;
const w2 = f2.stats.unexpected * 1000 + f2.stats.flaky;
return w2 - w1;
});
fs.writeFileSync(path.join(this._dataFolder, 'report.json'), JSON.stringify(htmlReport, undefined, 2));
// Copy app.
const appFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'webpack', 'htmlReport');
for (const file of fs.readdirSync(appFolder)) {
if (file.endsWith('.map'))
continue;
fs.copyFileSync(path.join(appFolder, file), path.join(this._reportFolder, file));
}
// Copy trace viewer.
if (this._hasTraces) {
const traceViewerFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'webpack', 'traceViewer');
const traceViewerTargetFolder = path.join(this._reportFolder, 'trace');
fs.mkdirSync(traceViewerTargetFolder, { recursive: true });
for (const file of fs.readdirSync(traceViewerFolder)) {
if (file.endsWith('.map'))
continue;
fs.copyFileSync(path.join(traceViewerFolder, file), path.join(traceViewerTargetFolder, file));
}
}
return ok;
}
@ -322,6 +335,8 @@ class HtmlBuilder {
error: result.error,
status: result.status,
attachments: result.attachments.map(a => {
if (a.name === 'trace')
this._hasTraces = true;
if (a.path) {
let fileName = a.path;
try {
@ -387,4 +402,15 @@ const emptyStats = (): Stats => {
};
};
const addStats = (stats: Stats, delta: Stats): Stats => {
stats.total += delta.total;
stats.skipped += delta.skipped;
stats.expected += delta.expected;
stats.unexpected += delta.unexpected;
stats.flaky += delta.flaky;
stats.ok = stats.ok && delta.ok;
stats.duration += delta.duration;
return stats;
};
export default HtmlReporter;

View File

@ -64,6 +64,7 @@ test('should generate report', async ({ runInlineTest }, testInfo) => {
delete reportObject.testIdToFileId;
delete reportObject.files[0].fileId;
delete reportObject.files[0].stats.duration;
delete reportObject.stats.duration;
const fileNames = new Set<string>();
for (const test of reportObject.files[0].tests) {
@ -129,7 +130,15 @@ test('should generate report', async ({ runInlineTest }, testInfo) => {
],
projectNames: [
'project-name'
]
],
stats: {
expected: 1,
flaky: 1,
ok: false,
skipped: 1,
total: 4,
unexpected: 1,
}
});
});