mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-12 20:03:03 +03:00
chore(html): only copy trace viewer for reports with traces (#9579)
This commit is contained in:
parent
4364c5f248
commit
432fb453e4
@ -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>;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user