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;