feat(test-runner): render step titles (#8270)

This commit is contained in:
Pavel Feldman 2021-08-17 16:41:36 -07:00 committed by GitHub
parent 52d63aa376
commit 710cec80a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 180 additions and 159 deletions

View File

@ -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.

View File

@ -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);
});

View File

@ -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 {

View File

@ -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<TestCase, number>();
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) {

View File

@ -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',
]);
});

View File

@ -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 = '<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 = '<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 = '<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 = '<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 = '<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\":\"<stack>\"}}`,
`%% end {\"title\":\"First step\",\"category\":\"test.step\",\"error\":{\"message\":\"expect(received).toBe(expected) // Object.is equality\\n\\nExpected: 2\\nReceived: 1\",\"stack\":\"<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\":\"<stack>\"}}],\"error\":{\"message\":\"expect(received).toBe(expected) // Object.is equality\\n\\nExpected: 2\\nReceived: 1\",\"stack\":\"<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: [],
},
],
},

View File

@ -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,
/**

View File

@ -60,6 +60,8 @@ export interface TestResult {
export interface TestStep {
title: string;
titlePath(): string[];
parent?: TestStep;
category: string,
startTime: Date;
duration: number;