From 710cec80a09c632225a3dc59c84829102020e929 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 17 Aug 2021 16:41:36 -0700 Subject: [PATCH] feat(test-runner): render step titles (#8270) --- docs/src/test-reporter-api/class-teststep.md | 11 ++ src/test/dispatcher.ts | 9 +- src/test/reporters/base.ts | 11 +- src/test/reporters/list.ts | 76 +++++-- tests/playwright-test/list-reporter.spec.ts | 35 ++++ tests/playwright-test/reporter.spec.ts | 186 +++++------------- types/testReporter.d.ts | 9 + .../overrides-testReporter.d.ts | 2 + 8 files changed, 180 insertions(+), 159 deletions(-) diff --git a/docs/src/test-reporter-api/class-teststep.md b/docs/src/test-reporter-api/class-teststep.md index 9ab3b678da..504ee03d8f 100644 --- a/docs/src/test-reporter-api/class-teststep.md +++ b/docs/src/test-reporter-api/class-teststep.md @@ -10,6 +10,7 @@ Step category to differentiate steps with different origin and verbosity. Built- * `hook` for fixtures and hooks initialization and teardown * `expect` for expect calls * `pw:api` for Playwright API calls. +* `test.step` for test.step API calls. ## property: TestStep.duration - type: <[float]> @@ -21,6 +22,11 @@ Running time in milliseconds. An error thrown during the step execution, if any. +## property: TestStep.parent +- type: <[void]|[TestStep]> + +Parent step, if any. + ## property: TestStep.startTime - type: <[Date]> @@ -35,3 +41,8 @@ List of steps inside this step. - type: <[string]> User-friendly test step title. + +## method: TestStep.titlePath +- returns: <[Array]<[string]>> + +Returns a list of step titles from the root step down to this step. diff --git a/src/test/dispatcher.ts b/src/test/dispatcher.ts index c838b0f51a..2610722c66 100644 --- a/src/test/dispatcher.ts +++ b/src/test/dispatcher.ts @@ -282,16 +282,21 @@ export class Dispatcher { }); worker.on('stepBegin', (params: StepBeginPayload) => { const { test, result, steps, stepStack } = this._testById.get(params.testId)!; + const parentStep = [...stepStack].pop(); const step: TestStep = { title: params.title, + titlePath: () => { + const parentPath = parentStep?.titlePath() || []; + return [...parentPath, params.title]; + }, + parent: parentStep, category: params.category, startTime: new Date(params.wallTime), duration: 0, steps: [], }; steps.set(params.stepId, step); - const parentStep = [...stepStack].pop() || result; - parentStep.steps.push(step); + (parentStep || result).steps.push(step); stepStack.add(step); this._reporter.onStepBegin?.(test, result, step); }); diff --git a/src/test/reporters/base.ts b/src/test/reporters/base.ts index 4eefee5c19..de60912c7b 100644 --- a/src/test/reporters/base.ts +++ b/src/test/reporters/base.ts @@ -21,7 +21,7 @@ import fs from 'fs'; import milliseconds from 'ms'; import path from 'path'; import StackUtils from 'stack-utils'; -import { FullConfig, TestCase, Suite, TestResult, TestError, Reporter, FullResult } from '../../../types/testReporter'; +import { FullConfig, TestCase, Suite, TestResult, TestError, Reporter, FullResult, TestStep } from '../../../types/testReporter'; const stackUtils = new StackUtils(); @@ -195,12 +195,17 @@ function relativeTestPath(config: FullConfig, test: TestCase): string { return path.relative(config.rootDir, test.location.file) || path.basename(test.location.file); } -export function formatTestTitle(config: FullConfig, test: TestCase): string { +function stepSuffix(step: TestStep | undefined) { + const stepTitles = step ? step.titlePath() : []; + return stepTitles.map(t => ' › ' + t).join(''); +} + +export function formatTestTitle(config: FullConfig, test: TestCase, step?: TestStep): string { // root, project, file, ...describes, test const [, projectName, , ...titles] = test.titlePath(); const location = `${relativeTestPath(config, test)}:${test.location.line}:${test.location.column}`; const projectTitle = projectName ? `[${projectName}] › ` : ''; - return `${projectTitle}${location} › ${titles.join(' ')}`; + return `${projectTitle}${location} › ${titles.join(' ')}${stepSuffix(step)}`; } function formatTestHeader(config: FullConfig, test: TestCase, indent: string, index?: number): string { diff --git a/src/test/reporters/list.ts b/src/test/reporters/list.ts index 70d58c4164..df1385ed60 100644 --- a/src/test/reporters/list.ts +++ b/src/test/reporters/list.ts @@ -19,7 +19,7 @@ import colors from 'colors/safe'; // @ts-ignore import milliseconds from 'ms'; import { BaseReporter, formatTestTitle } from './base'; -import { FullConfig, FullResult, Suite, TestCase, TestResult } from '../../../types/testReporter'; +import { FullConfig, FullResult, Suite, TestCase, TestResult, TestStep } from '../../../types/testReporter'; // Allow it in the Visual Studio Code Terminal and the new Windows Terminal const DOES_NOT_SUPPORT_UTF8_IN_TERMINAL = process.platform === 'win32' && process.env.TERM_PROGRAM !== 'vscode' && !process.env.WT_SESSION; @@ -30,6 +30,12 @@ class ListReporter extends BaseReporter { private _lastRow = 0; private _testRows = new Map(); private _needNewLine = false; + private _liveTerminal: string | boolean | undefined; + + constructor() { + super(); + this._liveTerminal = process.stdout.isTTY || process.env.PWTEST_SKIP_TEST_OUTPUT; + } onBegin(config: FullConfig, suite: Suite) { super.onBegin(config, suite); @@ -37,7 +43,7 @@ class ListReporter extends BaseReporter { } onTestBegin(test: TestCase) { - if (process.stdout.isTTY) { + if (this._liveTerminal) { if (this._needNewLine) { this._needNewLine = false; process.stdout.write('\n'); @@ -58,12 +64,28 @@ class ListReporter extends BaseReporter { this._dumpToStdio(test, chunk, process.stdout); } + onStepBegin(test: TestCase, result: TestResult, step: TestStep) { + if (!this._liveTerminal) + return; + if (step.category !== 'test.step') + return; + this._updateTestLine(test, ' ' + colors.gray(formatTestTitle(this.config, test, step))); + } + + onStepEnd(test: TestCase, result: TestResult, step: TestStep) { + if (!this._liveTerminal) + return; + if (step.category !== 'test.step') + return; + this._updateTestLine(test, ' ' + colors.gray(formatTestTitle(this.config, test, step.parent))); + } + private _dumpToStdio(test: TestCase | undefined, chunk: string | Buffer, stream: NodeJS.WriteStream) { if (this.config.quiet) return; const text = chunk.toString('utf-8'); this._needNewLine = text[text.length - 1] !== '\n'; - if (process.stdout.isTTY) { + if (this._liveTerminal) { const newLineCount = text.split('\n').length - 1; this._lastRow += newLineCount; } @@ -86,25 +108,41 @@ class ListReporter extends BaseReporter { text = '\u001b[2K\u001b[0G' + colors.red(statusMark + title) + duration; } - const testRow = this._testRows.get(test)!; - // Go up if needed - if (process.stdout.isTTY && testRow !== this._lastRow) - process.stdout.write(`\u001B[${this._lastRow - testRow}A`); - // Erase line - if (process.stdout.isTTY) - process.stdout.write('\u001B[2K'); - if (!process.stdout.isTTY && this._needNewLine) { - this._needNewLine = false; + if (this._liveTerminal) { + this._updateTestLine(test, text); + } else { + if (this._needNewLine) { + this._needNewLine = false; + process.stdout.write('\n'); + } + process.stdout.write(text); process.stdout.write('\n'); } - process.stdout.write(text); + } + + private _updateTestLine(test: TestCase, line: string) { + if (process.env.PWTEST_SKIP_TEST_OUTPUT) + this._updateTestLineForTest(test,line); + else + this._updateTestLineForTTY(test,line); + } + + private _updateTestLineForTTY(test: TestCase, line: string) { + const testRow = this._testRows.get(test)!; + // Go up if needed + if (testRow !== this._lastRow) + process.stdout.write(`\u001B[${this._lastRow - testRow}A`); + // Erase line + process.stdout.write('\u001B[2K'); + process.stdout.write(line); // Go down if needed. - if (testRow !== this._lastRow) { - if (process.stdout.isTTY) - process.stdout.write(`\u001B[${this._lastRow - testRow}E`); - else - process.stdout.write('\n'); - } + if (testRow !== this._lastRow) + process.stdout.write(`\u001B[${this._lastRow - testRow}E`); + } + + private _updateTestLineForTest(test: TestCase, line: string) { + const testRow = this._testRows.get(test)!; + process.stdout.write(testRow + ' : ' + line + '\n'); } async onEnd(result: FullResult) { diff --git a/tests/playwright-test/list-reporter.spec.ts b/tests/playwright-test/list-reporter.spec.ts index 5c4713ebd0..32aa33e1e3 100644 --- a/tests/playwright-test/list-reporter.spec.ts +++ b/tests/playwright-test/list-reporter.spec.ts @@ -47,3 +47,38 @@ test('render each test with project name', async ({ runInlineTest }) => { expect(text).toContain(`- [bar] › a.test.ts:12:12 › skipped`); expect(result.exitCode).toBe(1); }); + +test('render steps', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + const { test } = pwt; + test('passes', async ({}) => { + await test.step('outer 1.0', async () => { + await test.step('inner 1.1', async () => {}); + await test.step('inner 1.1', async () => {}); + }); + await test.step('outer 2.0', async () => { + await test.step('inner 2.1', async () => {}); + await test.step('inner 2.1', async () => {}); + }); + }); + `, + }, { reporter: 'list' }); + const text = stripAscii(result.output); + const lines = text.split('\n').filter(l => l.startsWith('0 :')); + lines.pop(); // Remove last item that contains [v] and time in ms. + expect(lines).toEqual([ + '0 : a.test.ts:6:7 › passes › outer 1.0', + '0 : a.test.ts:6:7 › passes › outer 1.0 › inner 1.1', + '0 : a.test.ts:6:7 › passes › outer 1.0', + '0 : a.test.ts:6:7 › passes › outer 1.0 › inner 1.1', + '0 : a.test.ts:6:7 › passes › outer 1.0', + '0 : a.test.ts:6:7 › passes', + '0 : a.test.ts:6:7 › passes › outer 2.0', + '0 : a.test.ts:6:7 › passes › outer 2.0 › inner 2.1', + '0 : a.test.ts:6:7 › passes › outer 2.0', + '0 : a.test.ts:6:7 › passes › outer 2.0 › inner 2.1', + '0 : a.test.ts:6:7 › passes › outer 2.0', + '0 : a.test.ts:6:7 › passes', + ]); +}); diff --git a/tests/playwright-test/reporter.spec.ts b/tests/playwright-test/reporter.spec.ts index 66313c1f12..5c8d137c97 100644 --- a/tests/playwright-test/reporter.spec.ts +++ b/tests/playwright-test/reporter.spec.ts @@ -34,6 +34,32 @@ class Reporter { module.exports = Reporter; `; +const stepsReporterJS = ` +class Reporter { + onStdOut(chunk) { + process.stdout.write(chunk); + } + distillStep(step) { + return { + ...step, + startTime: undefined, + duration: undefined, + parent: undefined, + steps: step.steps.length ? step.steps.map(s => this.distillStep(s)) : undefined, + }; + } + onStepBegin(test, result, step) { + console.log('%%%% begin', JSON.stringify(this.distillStep(step))); + } + onStepEnd(test, result, step) { + if (step.error?.stack) + step.error.stack = ''; + console.log('%%%% end', JSON.stringify(this.distillStep(step))); + } +} +module.exports = Reporter; +`; + test('should work with custom reporter', async ({ runInlineTest }) => { const result = await runInlineTest({ 'reporter.ts': ` @@ -159,27 +185,8 @@ test('should load reporter from node_modules', async ({ runInlineTest }) => { }); test('should report expect steps', async ({ runInlineTest }) => { - const expectReporterJS = ` - class Reporter { - onStdOut(chunk) { - process.stdout.write(chunk); - } - onStepBegin(test, result, step) { - const copy = { ...step, startTime: undefined, duration: undefined, steps: undefined }; - console.log('%%%% begin', JSON.stringify(copy)); - } - onStepEnd(test, result, step) { - const copy = { ...step, startTime: undefined, duration: undefined, steps: undefined }; - if (copy.error?.stack) - copy.error.stack = ''; - console.log('%%%% end', JSON.stringify(copy)); - } - } - module.exports = Reporter; - `; - const result = await runInlineTest({ - 'reporter.ts': expectReporterJS, + 'reporter.ts': stepsReporterJS, 'playwright.config.ts': ` module.exports = { reporter: './reporter', @@ -219,46 +226,21 @@ test('should report expect steps', async ({ runInlineTest }) => { `%% begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`, - `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, + `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}]}`, `%% begin {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\"}`, `%% begin {\"title\":\"page.title\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"page.title\",\"category\":\"pw:api\"}`, - `%% end {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\"}`, + `%% end {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\",\"steps\":[{\"title\":\"page.title\",\"category\":\"pw:api\"}]}`, `%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, - `%% end {\"title\":\"After Hooks\",\"category\":\"hook\"}`, + `%% end {\"title\":\"After Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.close\",\"category\":\"pw:api\"}]}`, ]); }); test('should report api steps', async ({ runInlineTest }) => { - const expectReporterJS = ` - class Reporter { - onStdOut(chunk) { - process.stdout.write(chunk); - } - onTestBegin(test) { - console.log('%%%% test begin ' + test.title); - } - onTestEnd(test) { - console.log('%%%% test end ' + test.title); - } - onStepBegin(test, result, step) { - const copy = { ...step, startTime: undefined, duration: undefined, steps: undefined }; - console.log('%%%% begin', JSON.stringify(copy)); - } - onStepEnd(test, result, step) { - const copy = { ...step, startTime: undefined, duration: undefined, steps: undefined }; - if (copy.error?.stack) - copy.error.stack = ''; - console.log('%%%% end', JSON.stringify(copy)); - } - } - module.exports = Reporter; - `; - const result = await runInlineTest({ - 'reporter.ts': expectReporterJS, + 'reporter.ts': stepsReporterJS, 'playwright.config.ts': ` module.exports = { reporter: './reporter', @@ -294,11 +276,10 @@ test('should report api steps', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); expect(result.output.split('\n').filter(line => line.startsWith('%%')).map(stripEscapedAscii)).toEqual([ - `%%%% test begin pass`, `%% begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`, - `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, + `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}]}`, `%% begin {\"title\":\"page.setContent\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"page.setContent\",\"category\":\"pw:api\"}`, `%% begin {\"title\":\"page.click\",\"category\":\"pw:api\"}`, @@ -306,50 +287,26 @@ test('should report api steps', async ({ runInlineTest }) => { `%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, - `%% end {\"title\":\"After Hooks\",\"category\":\"hook\"}`, - `%%%% test end pass`, - `%%%% test begin pass1`, + `%% end {\"title\":\"After Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.close\",\"category\":\"pw:api\"}]}`, `%% begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"page.click\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"page.click\",\"category\":\"pw:api\"}`, `%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`, `%% end {\"title\":\"After Hooks\",\"category\":\"hook\"}`, - `%%%% test end pass1`, - `%%%% test begin pass2`, `%% begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"page.click\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"page.click\",\"category\":\"pw:api\"}`, `%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`, `%% end {\"title\":\"After Hooks\",\"category\":\"hook\"}`, - `%%%% test end pass2`, ]); }); test('should report api step failure', async ({ runInlineTest }) => { - const expectReporterJS = ` - class Reporter { - onStdOut(chunk) { - process.stdout.write(chunk); - } - onStepBegin(test, result, step) { - const copy = { ...step, startTime: undefined, duration: undefined, steps: undefined }; - console.log('%%%% begin', JSON.stringify(copy)); - } - onStepEnd(test, result, step) { - const copy = { ...step, startTime: undefined, duration: undefined, steps: undefined }; - if (copy.error?.stack) - copy.error.stack = ''; - console.log('%%%% end', JSON.stringify(copy)); - } - } - module.exports = Reporter; - `; - const result = await runInlineTest({ - 'reporter.ts': expectReporterJS, + 'reporter.ts': stepsReporterJS, 'playwright.config.ts': ` module.exports = { reporter: './reporter', @@ -369,7 +326,7 @@ test('should report api step failure', async ({ runInlineTest }) => { `%% begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`, - `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, + `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}]}`, `%% begin {\"title\":\"page.setContent\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"page.setContent\",\"category\":\"pw:api\"}`, `%% begin {\"title\":\"page.click\",\"category\":\"pw:api\"}`, @@ -377,32 +334,13 @@ test('should report api step failure', async ({ runInlineTest }) => { `%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, - `%% end {\"title\":\"After Hooks\",\"category\":\"hook\"}`, + `%% end {\"title\":\"After Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.close\",\"category\":\"pw:api\"}]}`, ]); }); test('should report test.step', async ({ runInlineTest }) => { - const expectReporterJS = ` - class Reporter { - onStdOut(chunk) { - process.stdout.write(chunk); - } - onStepBegin(test, result, step) { - const copy = { ...step, startTime: undefined, duration: undefined, steps: undefined }; - console.log('%%%% begin', JSON.stringify(copy)); - } - onStepEnd(test, result, step) { - const copy = { ...step, startTime: undefined, duration: undefined, steps: undefined }; - if (copy.error?.stack) - copy.error.stack = ''; - console.log('%%%% end', JSON.stringify(copy)); - } - } - module.exports = Reporter; - `; - const result = await runInlineTest({ - 'reporter.ts': expectReporterJS, + 'reporter.ts': stepsReporterJS, 'playwright.config.ts': ` module.exports = { reporter: './reporter', @@ -423,15 +361,15 @@ test('should report test.step', async ({ runInlineTest }) => { `%% begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`, - `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, + `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}]}`, `%% begin {\"title\":\"First step\",\"category\":\"test.step\"}`, `%% begin {\"title\":\"expect.toBe\",\"category\":\"expect\"}`, `%% end {\"title\":\"expect.toBe\",\"category\":\"expect\",\"error\":{\"message\":\"expect(received).toBe(expected) // Object.is equality\\n\\nExpected: 2\\nReceived: 1\",\"stack\":\"\"}}`, - `%% end {\"title\":\"First step\",\"category\":\"test.step\",\"error\":{\"message\":\"expect(received).toBe(expected) // Object.is equality\\n\\nExpected: 2\\nReceived: 1\",\"stack\":\"\"}}`, + `%% end {\"title\":\"First step\",\"category\":\"test.step\",\"steps\":[{\"title\":\"expect.toBe\",\"category\":\"expect\",\"error\":{\"message\":\"expect(received).toBe(expected) // Object.is equality\\n\\nExpected: 2\\nReceived: 1\",\"stack\":\"\"}}],\"error\":{\"message\":\"expect(received).toBe(expected) // Object.is equality\\n\\nExpected: 2\\nReceived: 1\",\"stack\":\"\"}}`, `%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, - `%% end {\"title\":\"After Hooks\",\"category\":\"hook\"}`, + `%% end {\"title\":\"After Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.close\",\"category\":\"pw:api\"}]}`, ]); }); @@ -442,6 +380,16 @@ test('should report api step hierarchy', async ({ runInlineTest }) => { this.suite = suite; } + distillStep(step) { + return { + ...step, + startTime: undefined, + duration: undefined, + parent: undefined, + steps: step.steps.length ? step.steps.map(s => this.distillStep(s)) : undefined, + }; + } + async onEnd() { const processSuite = (suite: Suite) => { for (const child of suite.suites) @@ -449,7 +397,7 @@ test('should report api step hierarchy', async ({ runInlineTest }) => { for (const test of suite.tests) { for (const result of test.results) { for (const step of result.steps) { - console.log('%% ' + JSON.stringify(step)); + console.log('%% ' + JSON.stringify(this.distillStep(step))); } } } @@ -484,84 +432,52 @@ test('should report api step hierarchy', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); const objects = result.output.split('\n').filter(line => line.startsWith('%% ')).map(line => line.substring(3).trim()).filter(Boolean).map(line => JSON.parse(line)); - const distill = step => { - step.duration = 1; - step.startTime = 'time'; - step.steps.forEach(distill); - }; - objects.forEach(distill); expect(objects).toEqual([ { category: 'hook', title: 'Before Hooks', - duration: 1, - startTime: 'time', steps: [ { category: 'pw:api', title: 'browserContext.newPage', - duration: 1, - startTime: 'time', - steps: [], }, ], }, { category: 'test.step', title: 'outer step 1', - duration: 1, - startTime: 'time', steps: [ { category: 'test.step', title: 'inner step 1.1', - duration: 1, - startTime: 'time', - steps: [], }, { category: 'test.step', title: 'inner step 1.2', - duration: 1, - startTime: 'time', - steps: [], }, ], }, { category: 'test.step', title: 'outer step 2', - duration: 1, - startTime: 'time', steps: [ { category: 'test.step', title: 'inner step 2.1', - duration: 1, - startTime: 'time', - steps: [], }, { category: 'test.step', title: 'inner step 2.2', - duration: 1, - startTime: 'time', - steps: [], }, ], }, { category: 'hook', title: 'After Hooks', - duration: 1, - startTime: 'time', steps: [ { category: 'pw:api', title: 'browserContext.close', - duration: 1, - startTime: 'time', - steps: [], }, ], }, diff --git a/types/testReporter.d.ts b/types/testReporter.d.ts index 3e8020da88..cae0ed58bd 100644 --- a/types/testReporter.d.ts +++ b/types/testReporter.d.ts @@ -227,11 +227,20 @@ export interface TestStep { * User-friendly test step title. */ title: string; + /** + * Returns a list of step titles from the root step down to this step. + */ + titlePath(): string[]; + /** + * Parent step, if any. + */ + parent?: TestStep; /** * Step category to differentiate steps with different origin and verbosity. Built-in categories are: * - `hook` for fixtures and hooks initialization and teardown * - `expect` for expect calls * - `pw:api` for Playwright API calls. + * - `test.step` for test.step API calls. */ category: string, /** diff --git a/utils/generate_types/overrides-testReporter.d.ts b/utils/generate_types/overrides-testReporter.d.ts index b9a05be7b9..41f114aa98 100644 --- a/utils/generate_types/overrides-testReporter.d.ts +++ b/utils/generate_types/overrides-testReporter.d.ts @@ -60,6 +60,8 @@ export interface TestResult { export interface TestStep { title: string; + titlePath(): string[]; + parent?: TestStep; category: string, startTime: Date; duration: number;