diff --git a/packages/playwright-core/src/web/htmlReport/htmlReport.css b/packages/playwright-core/src/web/htmlReport/htmlReport.css index 5a8ad997a8..99b4b36806 100644 --- a/packages/playwright-core/src/web/htmlReport/htmlReport.css +++ b/packages/playwright-core/src/web/htmlReport/htmlReport.css @@ -14,12 +14,14 @@ 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, - rgb(0 0 0 / 15%) 0px 6.1px 6.3px, - rgb(0 0 0 / 10%) 0px -2px 4px, - rgb(0 0 0 / 15%) 0px -6.1px 12px, - rgb(0 0 0 / 25%) 0px 27px 28px; + rgb(0 0 0 / 15%) 0px 6.1px 6.3px, + rgb(0 0 0 / 10%) 0px -2px 4px, + rgb(0 0 0 / 15%) 0px -6.1px 12px, + rgb(0 0 0 / 25%) 0px 27px 28px; --color-border-default: #d0d7de; --color-border-muted: #d8dee4; --color-canvas-subtle: #f6f8fa; @@ -30,10 +32,54 @@ body { --color-primer-border-active: #fd8c73; --color-success-fg: #1a7f37; --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; } +#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 { padding-left: 34px; margin-top: 20px; @@ -191,7 +237,7 @@ body { } .stats.expected { - color: var(--green); + color: var(--color-green); } .stats.unexpected { @@ -199,7 +245,7 @@ body { } .stats.flaky { - color: var(--yellow); + color: var(--color-yellow); } video, img { @@ -338,7 +384,7 @@ a.no-decorations { } .color-text-warning { - color: var(--yellow) !important; + color: var(--color-yellow) !important; } .color-fg-muted { diff --git a/packages/playwright-core/src/web/htmlReport/htmlReport.tsx b/packages/playwright-core/src/web/htmlReport/htmlReport.tsx index 285859e705..767621843e 100644 --- a/packages/playwright-core/src/web/htmlReport/htmlReport.tsx +++ b/packages/playwright-core/src/web/htmlReport/htmlReport.tsx @@ -55,7 +55,7 @@ export const Report: React.FC = () => { }}> - {!!report?.testIdToFileId && } + {!!report && } } ; @@ -85,17 +85,17 @@ const TestFileSummaryView: React.FC<{ expanded={isFileExpanded(file.fileId)} setExpanded={(expanded => setFileExpanded(file.fileId, expanded))} header={ + {msToString(file.stats.duration)} {file.fileName} - {msToString(file.stats.duration)} }> {file.tests.map((test, i) =>
+ {msToString(test.duration)} {statusIcon(test.outcome)} {test.title} — {test.path.join(' › ')} {report.projectNames.length > 1 && !!test.projectName && {test.projectName}} - {msToString(test.duration)}
)} ; @@ -110,7 +110,7 @@ const TestCaseView: React.FC<{ const testId = new URL(window.location.href).searchParams.get('testId'); if (!testId || testId === test?.testId) return; - const fileId = report.testIdToFileId[testId]; + const fileId = testId.split('-')[0]; if (!fileId) return; const result = await fetch(`/data/${fileId}.json`, { cache: 'no-cache' }); diff --git a/packages/playwright-core/src/web/htmlReport/index.tsx b/packages/playwright-core/src/web/htmlReport/index.tsx index bc2b14f293..1fd5ba4dcd 100644 --- a/packages/playwright-core/src/web/htmlReport/index.tsx +++ b/packages/playwright-core/src/web/htmlReport/index.tsx @@ -16,11 +16,8 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { applyTheme } from '../theme'; -import '../common.css'; import { Report } from './htmlReport'; (async () => { - applyTheme(); ReactDOM.render(, document.querySelector('#root')); })(); diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts index 0f1f1b5583..fd320e359e 100644 --- a/packages/playwright-test/src/reporters/html.ts +++ b/packages/playwright-test/src/reporters/html.ts @@ -21,7 +21,6 @@ import path from 'path'; import { FullConfig, Suite } from '../../types/testReporter'; import { HttpServer } from 'playwright-core/src/utils/httpServer'; import { calculateSha1, removeFolders } from 'playwright-core/src/utils/utils'; -import { toPosixPath } from './json'; import RawReporter, { JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep, JsonAttachment } from './raw'; import assert from 'assert'; @@ -44,7 +43,6 @@ export type Location = { export type HTMLReport = { files: TestFileSummary[]; stats: Stats; - testIdToFileId: { [key: string]: string }; projectNames: string[]; }; @@ -63,7 +61,6 @@ export type TestFileSummary = { export type TestCaseSummary = { testId: string, - fileId: string, title: string; path: string[]; projectName: string; @@ -197,8 +194,8 @@ class HtmlBuilder { const data = new Map(); for (const projectJson of rawReports) { for (const file of projectJson.suites) { - const fileName = this._relativeLocation(file.location).file; - const fileId = calculateSha1(fileName); + const fileName = file.location!.file; + const fileId = file.fileId; let fileEntry = data.get(fileId); if (!fileEntry) { fileEntry = { @@ -218,11 +215,9 @@ class HtmlBuilder { } let ok = true; - const testIdToFileId: { [key: string]: string } = {}; for (const [fileId, { testFile, testFileSummary }] of data) { const stats = testFileSummary.stats; for (const test of testFileSummary.tests) { - testIdToFileId[test.testId] = fileId; if (test.outcome === 'expected') ++stats.expected; if (test.outcome === 'skipped') @@ -250,7 +245,6 @@ class HtmlBuilder { } const htmlReport: HTMLReport = { files: [...data.values()].map(e => e.testFileSummary), - testIdToFileId, projectNames: rawReports.map(r => r.project.name), 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 { const duration = test.results.reduce((a, r) => a + r.duration, 0); this._tests.set(test.testId, test); - const location = this._relativeLocation(test.location); + const location = test.location; path = [location.file + ':' + location.line, ...path.slice(1)]; this._testPath.set(test.testId, path); return { testCase: { testId: test.testId, - fileId, title: test.title, projectName, location, @@ -313,7 +306,6 @@ class HtmlBuilder { }, testCaseSummary: { testId: test.testId, - fileId, title: test.title, projectName, location, @@ -378,16 +370,6 @@ class HtmlBuilder { 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 => { diff --git a/packages/playwright-test/src/reporters/raw.ts b/packages/playwright-test/src/reporters/raw.ts index 004fc6a00e..c5e87f391a 100644 --- a/packages/playwright-test/src/reporters/raw.ts +++ b/packages/playwright-test/src/reporters/raw.ts @@ -21,7 +21,7 @@ import { FullConfig, Location, Suite, TestCase, TestResult, TestStatus, TestStep import { assert, calculateSha1 } from 'playwright-core/src/utils/utils'; import { sanitizeForFilePath } from '../util'; import { formatResultFailure } from './base'; -import { serializePatterns } from './json'; +import { toPosixPath, serializePatterns } from './json'; export type JsonLocation = Location; export type JsonError = string; @@ -48,6 +48,7 @@ export type JsonProject = { }; export type JsonSuite = { + fileId: string; title: string; location?: JsonLocation; suites: JsonSuite[]; @@ -129,6 +130,7 @@ class RawReporter { } generateProjectReport(config: FullConfig, suite: Suite): JsonReport { + this.config = config; const project = (suite as any)._projectConfig as FullProject; assert(project, 'Internal Error: Invalid project structure'); const report: JsonReport = { @@ -150,20 +152,25 @@ class RawReporter { } private _serializeSuite(suite: Suite): JsonSuite { + const fileId = calculateSha1(suite.location!.file.split(path.sep).join('/')); + const location = this._relativeLocation(suite.location); return { title: suite.title, - location: suite.location, + fileId, + location, 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 { - const testId = calculateSha1(test.titlePath().join('|')); + private _serializeTest(test: TestCase, fileId: string, fileName: string): JsonTestCase { + const [, projectName, , ...titles] = test.titlePath(); + const testIdExpression = `project:${projectName}|path:${titles.join('>')}`; + const testId = fileId + '-' + calculateSha1(testIdExpression); return { testId, title: test.title, - location: test.location, + location: this._relativeLocation(test.location), expectedStatus: test.expectedStatus, timeout: test.timeout, annotations: test.annotations, @@ -240,6 +247,17 @@ class RawReporter { 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;