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/, + ]); +});