chore: minor UI updates (#9590)

This commit is contained in:
Pavel Feldman 2021-10-18 12:34:02 -08:00 committed by GitHub
parent 909b039b9a
commit 99a2bd604d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 86 additions and 43 deletions

View file

@ -14,12 +14,14 @@
limitations under the License. limitations under the License.
*/ */
body { :root {
--box-shadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px;
--monospace-font: "SF Mono", Monaco, Consolas, "Droid Sans Mono", Inconsolata, "Courier New",monospace;
--box-shadow-thick: rgb(0 0 0 / 10%) 0px 1.8px 1.9px, --box-shadow-thick: rgb(0 0 0 / 10%) 0px 1.8px 1.9px,
rgb(0 0 0 / 15%) 0px 6.1px 6.3px, rgb(0 0 0 / 15%) 0px 6.1px 6.3px,
rgb(0 0 0 / 10%) 0px -2px 4px, rgb(0 0 0 / 10%) 0px -2px 4px,
rgb(0 0 0 / 15%) 0px -6.1px 12px, rgb(0 0 0 / 15%) 0px -6.1px 12px,
rgb(0 0 0 / 25%) 0px 27px 28px; rgb(0 0 0 / 25%) 0px 27px 28px;
--color-border-default: #d0d7de; --color-border-default: #d0d7de;
--color-border-muted: #d8dee4; --color-border-muted: #d8dee4;
--color-canvas-subtle: #f6f8fa; --color-canvas-subtle: #f6f8fa;
@ -30,10 +32,54 @@ body {
--color-primer-border-active: #fd8c73; --color-primer-border-active: #fd8c73;
--color-success-fg: #1a7f37; --color-success-fg: #1a7f37;
--color-accent-fg: #0969da; --color-accent-fg: #0969da;
color: var(--color-fg-default); --color-green: #367c39;
--color-yellow: #ff9207;
}
html, body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overscroll-behavior-x: none;
}
body {
overflow: auto; overflow: auto;
} }
#root {
width: 100%;
height: 100%;
color: var(--color-fg-default);
font-size: 14px;
font-family: SegoeUI-SemiBold-final,Segoe UI Semibold,SegoeUI-Regular-final,Segoe UI,"Segoe UI Web (West European)",Segoe,-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,Tahoma,Helvetica,Arial,sans-serif;
-webkit-font-smoothing: antialiased;
}
* {
box-sizing: border-box;
min-width: 0;
min-height: 0;
}
svg {
fill: currentColor;
}
.vbox {
display: flex;
flex-direction: column;
flex: auto;
position: relative;
}
.hbox {
display: flex;
flex: auto;
position: relative;
}
.global-stats { .global-stats {
padding-left: 34px; padding-left: 34px;
margin-top: 20px; margin-top: 20px;
@ -191,7 +237,7 @@ body {
} }
.stats.expected { .stats.expected {
color: var(--green); color: var(--color-green);
} }
.stats.unexpected { .stats.unexpected {
@ -199,7 +245,7 @@ body {
} }
.stats.flaky { .stats.flaky {
color: var(--yellow); color: var(--color-yellow);
} }
video, img { video, img {
@ -338,7 +384,7 @@ a.no-decorations {
} }
.color-text-warning { .color-text-warning {
color: var(--yellow) !important; color: var(--color-yellow) !important;
} }
.color-fg-muted { .color-fg-muted {

View file

@ -55,7 +55,7 @@ export const Report: React.FC = () => {
}}></AllTestFilesSummaryView> }}></AllTestFilesSummaryView>
</Route> </Route>
<Route params='testId'> <Route params='testId'>
{!!report?.testIdToFileId && <TestCaseView report={report}></TestCaseView>} {!!report && <TestCaseView report={report}></TestCaseView>}
</Route> </Route>
</div>} </div>}
</div>; </div>;
@ -85,17 +85,17 @@ const TestFileSummaryView: React.FC<{
expanded={isFileExpanded(file.fileId)} expanded={isFileExpanded(file.fileId)}
setExpanded={(expanded => setFileExpanded(file.fileId, expanded))} setExpanded={(expanded => setFileExpanded(file.fileId, expanded))}
header={<span> header={<span>
<span style={{ float: 'right' }}>{msToString(file.stats.duration)}</span>
{file.fileName} {file.fileName}
<StatsView stats={file.stats}></StatsView> <StatsView stats={file.stats}></StatsView>
<span style={{ float: 'right' }}>{msToString(file.stats.duration)}</span>
</span>}> </span>}>
{file.tests.map((test, i) => <Link key={`test-${i}`} href={`/?testId=${test.testId}`}> {file.tests.map((test, i) => <Link key={`test-${i}`} href={`/?testId=${test.testId}`}>
<div className={'test-summary outcome-' + test.outcome}> <div className={'test-summary outcome-' + test.outcome}>
<span style={{ float: 'right' }}>{msToString(test.duration)}</span>
{statusIcon(test.outcome)} {statusIcon(test.outcome)}
{test.title} {test.title}
<span className='test-summary-path'> {test.path.join(' ')}</span> <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>} {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> </div>
</Link>)} </Link>)}
</Chip>; </Chip>;
@ -110,7 +110,7 @@ const TestCaseView: React.FC<{
const testId = new URL(window.location.href).searchParams.get('testId'); const testId = new URL(window.location.href).searchParams.get('testId');
if (!testId || testId === test?.testId) if (!testId || testId === test?.testId)
return; return;
const fileId = report.testIdToFileId[testId]; const fileId = testId.split('-')[0];
if (!fileId) if (!fileId)
return; return;
const result = await fetch(`/data/${fileId}.json`, { cache: 'no-cache' }); const result = await fetch(`/data/${fileId}.json`, { cache: 'no-cache' });

View file

@ -16,11 +16,8 @@
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom';
import { applyTheme } from '../theme';
import '../common.css';
import { Report } from './htmlReport'; import { Report } from './htmlReport';
(async () => { (async () => {
applyTheme();
ReactDOM.render(<Report />, document.querySelector('#root')); ReactDOM.render(<Report />, document.querySelector('#root'));
})(); })();

View file

@ -21,7 +21,6 @@ import path from 'path';
import { FullConfig, Suite } from '../../types/testReporter'; import { FullConfig, Suite } from '../../types/testReporter';
import { HttpServer } from 'playwright-core/src/utils/httpServer'; import { HttpServer } from 'playwright-core/src/utils/httpServer';
import { calculateSha1, removeFolders } from 'playwright-core/src/utils/utils'; import { calculateSha1, removeFolders } from 'playwright-core/src/utils/utils';
import { toPosixPath } from './json';
import RawReporter, { JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep, JsonAttachment } from './raw'; import RawReporter, { JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep, JsonAttachment } from './raw';
import assert from 'assert'; import assert from 'assert';
@ -44,7 +43,6 @@ export type Location = {
export type HTMLReport = { export type HTMLReport = {
files: TestFileSummary[]; files: TestFileSummary[];
stats: Stats; stats: Stats;
testIdToFileId: { [key: string]: string };
projectNames: string[]; projectNames: string[];
}; };
@ -63,7 +61,6 @@ export type TestFileSummary = {
export type TestCaseSummary = { export type TestCaseSummary = {
testId: string, testId: string,
fileId: string,
title: string; title: string;
path: string[]; path: string[];
projectName: string; projectName: string;
@ -197,8 +194,8 @@ class HtmlBuilder {
const data = new Map<string, { testFile: TestFile, testFileSummary: TestFileSummary }>(); const data = new Map<string, { testFile: TestFile, testFileSummary: TestFileSummary }>();
for (const projectJson of rawReports) { for (const projectJson of rawReports) {
for (const file of projectJson.suites) { for (const file of projectJson.suites) {
const fileName = this._relativeLocation(file.location).file; const fileName = file.location!.file;
const fileId = calculateSha1(fileName); const fileId = file.fileId;
let fileEntry = data.get(fileId); let fileEntry = data.get(fileId);
if (!fileEntry) { if (!fileEntry) {
fileEntry = { fileEntry = {
@ -218,11 +215,9 @@ class HtmlBuilder {
} }
let ok = true; let ok = true;
const testIdToFileId: { [key: string]: string } = {};
for (const [fileId, { testFile, testFileSummary }] of data) { for (const [fileId, { testFile, testFileSummary }] of data) {
const stats = testFileSummary.stats; const stats = testFileSummary.stats;
for (const test of testFileSummary.tests) { for (const test of testFileSummary.tests) {
testIdToFileId[test.testId] = fileId;
if (test.outcome === 'expected') if (test.outcome === 'expected')
++stats.expected; ++stats.expected;
if (test.outcome === 'skipped') if (test.outcome === 'skipped')
@ -250,7 +245,6 @@ class HtmlBuilder {
} }
const htmlReport: HTMLReport = { const htmlReport: HTMLReport = {
files: [...data.values()].map(e => e.testFileSummary), 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()) stats: [...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats())
}; };
@ -294,14 +288,13 @@ class HtmlBuilder {
private _createTestEntry(test: JsonTestCase, fileId: string, projectName: string, path: string[]): TestEntry { private _createTestEntry(test: JsonTestCase, fileId: string, projectName: string, path: string[]): TestEntry {
const duration = test.results.reduce((a, r) => a + r.duration, 0); const duration = test.results.reduce((a, r) => a + r.duration, 0);
this._tests.set(test.testId, test); this._tests.set(test.testId, test);
const location = this._relativeLocation(test.location); const location = test.location;
path = [location.file + ':' + location.line, ...path.slice(1)]; path = [location.file + ':' + location.line, ...path.slice(1)];
this._testPath.set(test.testId, path); this._testPath.set(test.testId, path);
return { return {
testCase: { testCase: {
testId: test.testId, testId: test.testId,
fileId,
title: test.title, title: test.title,
projectName, projectName,
location, location,
@ -313,7 +306,6 @@ class HtmlBuilder {
}, },
testCaseSummary: { testCaseSummary: {
testId: test.testId, testId: test.testId,
fileId,
title: test.title, title: test.title,
projectName, projectName,
location, location,
@ -378,16 +370,6 @@ class HtmlBuilder {
error: step.error error: step.error
}; };
} }
private _relativeLocation(location: Location | undefined): Location {
if (!location)
return { file: '', line: 0, column: 0 };
return {
file: toPosixPath(path.relative(this._rootDir, location.file)),
line: location.line,
column: location.column,
};
}
} }
const emptyStats = (): Stats => { const emptyStats = (): Stats => {

View file

@ -21,7 +21,7 @@ import { FullConfig, Location, Suite, TestCase, TestResult, TestStatus, TestStep
import { assert, calculateSha1 } from 'playwright-core/src/utils/utils'; import { assert, calculateSha1 } from 'playwright-core/src/utils/utils';
import { sanitizeForFilePath } from '../util'; import { sanitizeForFilePath } from '../util';
import { formatResultFailure } from './base'; import { formatResultFailure } from './base';
import { serializePatterns } from './json'; import { toPosixPath, serializePatterns } from './json';
export type JsonLocation = Location; export type JsonLocation = Location;
export type JsonError = string; export type JsonError = string;
@ -48,6 +48,7 @@ export type JsonProject = {
}; };
export type JsonSuite = { export type JsonSuite = {
fileId: string;
title: string; title: string;
location?: JsonLocation; location?: JsonLocation;
suites: JsonSuite[]; suites: JsonSuite[];
@ -129,6 +130,7 @@ class RawReporter {
} }
generateProjectReport(config: FullConfig, suite: Suite): JsonReport { generateProjectReport(config: FullConfig, suite: Suite): JsonReport {
this.config = config;
const project = (suite as any)._projectConfig as FullProject; const project = (suite as any)._projectConfig as FullProject;
assert(project, 'Internal Error: Invalid project structure'); assert(project, 'Internal Error: Invalid project structure');
const report: JsonReport = { const report: JsonReport = {
@ -150,20 +152,25 @@ class RawReporter {
} }
private _serializeSuite(suite: Suite): JsonSuite { private _serializeSuite(suite: Suite): JsonSuite {
const fileId = calculateSha1(suite.location!.file.split(path.sep).join('/'));
const location = this._relativeLocation(suite.location);
return { return {
title: suite.title, title: suite.title,
location: suite.location, fileId,
location,
suites: suite.suites.map(s => this._serializeSuite(s)), suites: suite.suites.map(s => this._serializeSuite(s)),
tests: suite.tests.map(t => this._serializeTest(t)), tests: suite.tests.map(t => this._serializeTest(t, fileId, location.file)),
}; };
} }
private _serializeTest(test: TestCase): JsonTestCase { private _serializeTest(test: TestCase, fileId: string, fileName: string): JsonTestCase {
const testId = calculateSha1(test.titlePath().join('|')); const [, projectName, , ...titles] = test.titlePath();
const testIdExpression = `project:${projectName}|path:${titles.join('>')}`;
const testId = fileId + '-' + calculateSha1(testIdExpression);
return { return {
testId, testId,
title: test.title, title: test.title,
location: test.location, location: this._relativeLocation(test.location),
expectedStatus: test.expectedStatus, expectedStatus: test.expectedStatus,
timeout: test.timeout, timeout: test.timeout,
annotations: test.annotations, annotations: test.annotations,
@ -240,6 +247,17 @@ class RawReporter {
body: chunk.toString('base64') body: chunk.toString('base64')
}; };
} }
private _relativeLocation(location: Location | undefined): Location {
if (!location)
return { file: '', line: 0, column: 0 };
const file = toPosixPath(path.relative(this.config.rootDir, location.file));
return {
file,
line: location.line,
column: location.column,
};
}
} }
export default RawReporter; export default RawReporter;