diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx
index 628cb976fc..64eb73a855 100644
--- a/packages/html-reporter/src/testCaseView.spec.tsx
+++ b/packages/html-reporter/src/testCaseView.spec.tsx
@@ -31,12 +31,14 @@ const result: TestResult = {
startTime: new Date(100).toUTCString(),
duration: 10,
location: { file: 'test.spec.ts', line: 62, column: 0 },
+ count: 1,
steps: [{
title: 'Inner step',
startTime: new Date(200).toUTCString(),
duration: 10,
location: { file: 'test.spec.ts', line: 82, column: 0 },
steps: [],
+ count: 1,
}],
}],
attachments: [],
diff --git a/packages/html-reporter/src/testResultView.css b/packages/html-reporter/src/testResultView.css
index c89e13341e..9f07a89cda 100644
--- a/packages/html-reporter/src/testResultView.css
+++ b/packages/html-reporter/src/testResultView.css
@@ -57,6 +57,24 @@
line-height: initial;
}
+.test-result-counter {
+ border-radius: 12px;
+ color: var(--color-canvas-default);
+ padding: 2px 8px;
+}
+
+@media(prefers-color-scheme: light) {
+ .test-result-counter {
+ background: var(--color-scale-gray-5);
+ }
+}
+
+@media(prefers-color-scheme: dark) {
+ .test-result-counter {
+ background: var(--color-scale-gray-3);
+ }
+}
+
@media only screen and (max-width: 600px) {
.test-result {
padding: 0 !important;
diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx
index 64cc27109d..30314ae0b2 100644
--- a/packages/html-reporter/src/testResultView.tsx
+++ b/packages/html-reporter/src/testResultView.tsx
@@ -105,6 +105,7 @@ const StepTreeItem: React.FC<{
{msToString(step.duration)}
{statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')}
{step.title}
+ {step.count > 1 && <> ✕ {step.count}>}
{step.location && — {step.location.file}:{step.location.line}}
} loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => {
const children = step.steps.map((s, i) => );
diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts
index 47fc3316f8..29b656a47a 100644
--- a/packages/playwright-test/src/reporters/html.ts
+++ b/packages/playwright-test/src/reporters/html.ts
@@ -106,6 +106,7 @@ export type TestStep = {
snippet?: string;
error?: string;
steps: TestStep[];
+ count: number;
};
type TestEntry = {
@@ -478,7 +479,8 @@ class HtmlBuilder {
snippet: step.snippet,
steps: step.steps.map(s => this._createTestStep(s)),
location: step.location,
- error: step.error
+ error: step.error,
+ count: step.count
};
}
}
diff --git a/packages/playwright-test/src/reporters/raw.ts b/packages/playwright-test/src/reporters/raw.ts
index d62c51c220..300600fdae 100644
--- a/packages/playwright-test/src/reporters/raw.ts
+++ b/packages/playwright-test/src/reporters/raw.ts
@@ -97,6 +97,7 @@ export type JsonTestStep = {
steps: JsonTestStep[];
location?: Location;
snippet?: string;
+ count: number;
};
class RawReporter {
@@ -220,7 +221,7 @@ class RawReporter {
status: result.status,
error: formatResultFailure(test, result, '', true).tokens.join('').trim(),
attachments: this._createAttachments(result),
- steps: result.steps.map(step => this._serializeStep(test, step))
+ steps: dedupeSteps(result.steps.map(step => this._serializeStep(test, step)))
};
}
@@ -232,7 +233,8 @@ class RawReporter {
duration: step.duration,
error: step.error?.message,
location: this._relativeLocation(step.location),
- steps: step.steps.map(step => this._serializeStep(test, step)),
+ steps: dedupeSteps(step.steps.map(step => this._serializeStep(test, step))),
+ count: 1
};
if (step.location)
@@ -292,4 +294,20 @@ class RawReporter {
}
}
+function dedupeSteps(steps: JsonTestStep[]): JsonTestStep[] {
+ const result: JsonTestStep[] = [];
+ let lastStep: JsonTestStep | undefined;
+ for (const step of steps) {
+ const canDedupe = !step.error && step.duration >= 0 && step.location?.file && !step.steps.length;
+ if (canDedupe && lastStep && step.category === lastStep.category && step.title === lastStep.title && step.location?.file === lastStep.location?.file && step.location?.line === lastStep.location?.line && step.location?.column === lastStep.location?.column) {
+ ++lastStep.count;
+ lastStep.duration += step.duration;
+ continue;
+ }
+ result.push(step);
+ lastStep = canDedupe ? step : undefined;
+ }
+ return result;
+}
+
export default RawReporter;
diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts
index adbeede312..463d0c4316 100644
--- a/tests/playwright-test/reporter-html.spec.ts
+++ b/tests/playwright-test/reporter-html.spec.ts
@@ -447,3 +447,26 @@ test('should differentiate repeat-each test cases', async ({ runInlineTest, show
await expect(page.locator('text=Before Hooks')).toBeVisible();
await expect(page.locator('text=ouch')).toBeHidden();
});
+
+test('should group similar / loop steps', async ({ runInlineTest, showReport, page }) => {
+ test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/10098' });
+ const result = await runInlineTest({
+ 'a.spec.js': `
+ const { test } = pwt;
+ test('sample', async ({}, testInfo) => {
+ for (let i = 0; i < 10; ++i)
+ expect(1).toBe(1);
+ for (let i = 0; i < 20; ++i)
+ expect(2).toEqual(2);
+ });
+ `
+ }, { 'reporter': 'dot,html' });
+ expect(result.exitCode).toBe(0);
+ await showReport();
+
+ await page.locator('text=sample').first().click();
+ await expect(page.locator('.tree-item-title')).toContainText([
+ /expect\.toBe.*10/,
+ /expect\.toEqual.*20/,
+ ]);
+});