diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md index f4af86db49..78cb947d0e 100644 --- a/docs/src/test-api/class-test.md +++ b/docs/src/test-api/class-test.md @@ -1374,7 +1374,50 @@ Step name. Step body. +### option: Test.step.box +* since: v1.39 +- `box` +Whether to box the step in the report. Defaults to `false`. When the step is boxed, errors thrown from the step internals point to the step call site. + +```js +const assertGoodPage = async page => { + await test.step('assertGoodPage', async () => { + await expect(page.getByText('does-not-exist')).toBeVisible(); + }, { box: true }); +}; + +test('box', async ({ page }) => { + await assertGoodPage(page); // <-- Errors will be reported on this line. +}); +``` + +You can also use TypeScript method decorators to annotate method as a boxed step: + +```js +function boxedStep(target: Function, context: ClassMethodDecoratorContext) { + return function replacementMethod(...args: any) { + const name = this.constructor.name + '.' + (context.name as string); + return test.step(name, async () => { + return await target.call(this, ...args); + }, { box: true }); + }; +} + +class Pom { + constructor(readonly page: Page) {} + + @boxedStep + async assertGoodPage() { + await expect(this.page.getByText('does-not-exist')).toBeVisible({ timeout: 1 }); + } +} + +test('box', async ({ page }) => { + const pom = new Pom(page); + await pom.assertGoodPage(); // <-- Error will be reported on this line. +}); +``` ## method: Test.use * since: v1.10 diff --git a/packages/playwright/src/common/testType.ts b/packages/playwright/src/common/testType.ts index 05fcb54a04..7d32cf7ba2 100644 --- a/packages/playwright/src/common/testType.ts +++ b/packages/playwright/src/common/testType.ts @@ -54,7 +54,7 @@ export class TestTypeImpl { test.fail = wrapFunctionWithLocation(this._modifier.bind(this, 'fail')); test.slow = wrapFunctionWithLocation(this._modifier.bind(this, 'slow')); test.setTimeout = wrapFunctionWithLocation(this._setTimeout.bind(this)); - test.step = wrapFunctionWithLocation(this._step.bind(this)); + test.step = this._step.bind(this); test.use = wrapFunctionWithLocation(this._use.bind(this)); test.extend = wrapFunctionWithLocation(this._extend.bind(this)); test._extendTest = wrapFunctionWithLocation(this._extendTest.bind(this)); @@ -219,11 +219,11 @@ export class TestTypeImpl { suite._use.push({ fixtures, location }); } - private async _step(location: Location, title: string, body: () => Promise): Promise { + async _step(title: string, body: () => Promise, options: { box?: boolean } = {}): Promise { const testInfo = currentTestInfo(); if (!testInfo) throw new Error(`test.step() can only be called from a test`); - return testInfo._runAsStep({ category: 'test.step', title, location }, async step => { + return testInfo._runAsStep({ category: 'test.step', title, box: options.box }, async () => { // Make sure that internal "step" is not leaked to the user callback. return await body(); }); diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 649f20e19d..bd148f1bb3 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -24,7 +24,7 @@ import { TimeoutManager } from './timeoutManager'; import type { RunnableType, TimeSlot } from './timeoutManager'; import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config'; import type { Location } from '../../types/testReporter'; -import { getContainedPath, normalizeAndSaveAttachment, serializeError, trimLongString } from '../util'; +import { filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, serializeError, stringifyStackFrames, trimLongString } from '../util'; import { TestTracing } from './testTracing'; import type { Attachment } from './testTracing'; @@ -42,6 +42,7 @@ export interface TestStepInternal { params?: Record; error?: TestInfoError; infectParentStepsWithError?: boolean; + box?: boolean; } export class TestInfoImpl implements TestInfo { @@ -245,8 +246,12 @@ export class TestInfoImpl implements TestInfo { _addStep(data: Omit, parentStep?: TestStepInternal): TestStepInternal { const stepId = `${data.category}@${++this._lastStepId}`; + const rawStack = data.box || !data.location || !parentStep ? captureRawStack() : null; + const filteredStack = rawStack ? filteredStackTrace(rawStack) : []; if (!parentStep) - parentStep = zones.zoneData('stepZone', captureRawStack()) || undefined; + parentStep = zones.zoneData('stepZone', rawStack!) || undefined; + const boxedStack = data.box ? filteredStack.slice(1) : undefined; + const location = data.location || boxedStack?.[0] || filteredStack[0]; // For out-of-stack calls, locate the enclosing step. let isLaxParent = false; @@ -266,6 +271,7 @@ export class TestInfoImpl implements TestInfo { const step: TestStepInternal = { stepId, ...data, + location, laxParent: isLaxParent, steps: [], complete: result => { @@ -275,6 +281,10 @@ export class TestInfoImpl implements TestInfo { let error: TestInfoError | undefined; if (result.error instanceof Error) { // Step function threw an error. + if (boxedStack) { + const errorTitle = `${result.error.name}: ${result.error.message}`; + result.error.stack = `${errorTitle}\n${stringifyStackFrames(boxedStack).join('\n')}`; + } error = serializeError(result.error); } else if (result.error) { // Internal API step reported an error. @@ -309,9 +319,6 @@ export class TestInfoImpl implements TestInfo { }; const parentStepList = parentStep ? parentStep.steps : this._steps; parentStepList.push(step); - const hasLocation = data.location && !data.location.file.includes('@playwright'); - // Sanitize location that comes from user land, it might have extra properties. - const location = data.location && hasLocation ? { file: data.location.file, line: data.location.line, column: data.location.column } : undefined; const payload: StepBeginPayload = { testId: this._test.id, stepId, @@ -322,7 +329,7 @@ export class TestInfoImpl implements TestInfo { location, }; this._onStepBegin(payload); - this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.apiName || data.title, data.params, data.wallTime, data.location ? [data.location] : []); + this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.apiName || data.title, data.params, data.wallTime, location ? [location] : []); return step; } @@ -372,7 +379,7 @@ export class TestInfoImpl implements TestInfo { step.complete({}); return result; } catch (e) { - step.complete({ error: e instanceof SkipError ? undefined : serializeError(e) }); + step.complete({ error: e instanceof SkipError ? undefined : e }); throw e; } }); diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 26b74528cd..43f4818b7f 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -3349,8 +3349,9 @@ export interface TestType(title: string, body: () => T | Promise): Promise; + step(title: string, body: () => T | Promise, options?: { box?: boolean }): Promise; /** * `expect` function can be used to create test assertions. Read more about [test assertions](https://playwright.dev/docs/test-assertions). * diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index ff3ca04f42..c22270277e 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -17,6 +17,11 @@ import { test, expect } from './playwright-test-fixtures'; const stepHierarchyReporter = ` +const asciiRegex = new RegExp('[\\\\u001B\\\\u009B][[\\\\]()#;?]*(?:(?:(?:[a-zA-Z\\\\d]*(?:;[-a-zA-Z\\\\d\\\\/#&.:=?%@~_]*)*)?\\\\u0007)|(?:(?:\\\\d{1,4}(?:;\\\\d{0,4})*)?[\\\\dA-PR-TZcf-ntqry=><~]))', 'g'); +export function stripAnsi(str) { + return str.replace(asciiRegex, ''); +} + class Reporter { onBegin(config: FullConfig, suite: Suite) { this.suite = suite; @@ -31,11 +36,11 @@ class Reporter { data: undefined, location: step.location ? { file: step.location.file.substring(step.location.file.lastIndexOf(require('path').sep) + 1).replace('.js', '.ts'), - line: step.location.line ? typeof step.location.line : 0, - column: step.location.column ? typeof step.location.column : 0 + line: step.location.line, + column: step.location.column } : undefined, steps: step.steps.length ? step.steps.map(s => this.distillStep(s)) : undefined, - error: step.error ? '' : undefined, + error: step.error ? stripAnsi(step.error.stack || '') : undefined, }; } @@ -131,26 +136,26 @@ test('should report api step hierarchy', async ({ runInlineTest }) => { category: 'test.step', title: 'outer step 1', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, steps: [ { category: 'test.step', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, title: 'inner step 1.1', }, { category: 'test.step', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, title: 'inner step 1.2', }, @@ -160,26 +165,26 @@ test('should report api step hierarchy', async ({ runInlineTest }) => { category: 'test.step', title: 'outer step 2', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, steps: [ { category: 'test.step', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, title: 'inner step 2.1', }, { category: 'test.step', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, title: 'inner step 2.2', }, @@ -226,16 +231,16 @@ test('should report before hooks step error', async ({ runInlineTest }) => { { category: 'hook', title: 'Before Hooks', - error: '', + error: expect.any(String), steps: [ { category: 'hook', title: 'beforeEach hook', - error: '', + error: expect.any(String), location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, } ], @@ -308,9 +313,9 @@ test('should not report nested after hooks', async ({ runInlineTest }) => { category: 'test.step', title: 'my step', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, }, { @@ -453,9 +458,9 @@ test('should report expect step locations', async ({ runInlineTest }) => { category: 'expect', title: 'expect.toBeTruthy', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, }, { @@ -531,21 +536,21 @@ test('should report custom expect steps', async ({ runInlineTest }) => { { category: 'expect', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, title: 'expect.toBeWithinRange', }, { category: 'expect', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, title: 'expect.toBeFailingAsync', - error: '', + error: expect.any(String), }, { category: 'hook', @@ -606,27 +611,27 @@ test('should mark step as failed when soft expect fails', async ({ runInlineTest { title: 'outer', category: 'test.step', - error: '', + error: expect.any(String), steps: [{ title: 'inner', category: 'test.step', - error: '', + error: expect.any(String), steps: [ { title: 'expect.soft.toBe', category: 'expect', - location: { file: 'a.test.ts', line: 'number', column: 'number' }, - error: '' + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) }, + error: expect.any(String) } ], - location: { file: 'a.test.ts', line: 'number', column: 'number' } + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) } }], - location: { file: 'a.test.ts', line: 'number', column: 'number' } + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) } }, { title: 'passing', category: 'test.step', - location: { file: 'a.test.ts', line: 'number', column: 'number' } + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) } }, { title: 'After Hooks', category: 'hook' } ]); @@ -691,10 +696,10 @@ test('should nest steps based on zones', async ({ runInlineTest }) => { { title: 'in beforeAll', category: 'test.step', - location: { file: 'a.test.ts', line: 'number', column: 'number' } + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) } } ], - location: { file: 'a.test.ts', line: 'number', column: 'number' } + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) } }, { title: 'beforeEach hook', @@ -703,10 +708,10 @@ test('should nest steps based on zones', async ({ runInlineTest }) => { { title: 'in beforeEach', category: 'test.step', - location: { file: 'a.test.ts', line: 'number', column: 'number' } + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) } } ], - location: { file: 'a.test.ts', line: 'number', column: 'number' } + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) } }, { category: 'fixture', @@ -751,20 +756,20 @@ test('should nest steps based on zones', async ({ runInlineTest }) => { { title: 'child1', category: 'test.step', - location: { file: 'a.test.ts', line: 'number', column: 'number' }, + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) }, steps: [ { title: 'page.click(body)', category: 'pw:api', - location: { file: 'a.test.ts', line: 'number', column: 'number' } + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) } } ] } ], location: { file: 'a.test.ts', - line: 'number', - column: 'number' + line: expect.any(Number), + column: expect.any(Number) } }, { @@ -774,23 +779,23 @@ test('should nest steps based on zones', async ({ runInlineTest }) => { { title: 'child2', category: 'test.step', - location: { file: 'a.test.ts', line: 'number', column: 'number' }, + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) }, steps: [ { title: 'expect.toBeVisible', category: 'expect', - location: { file: 'a.test.ts', line: 'number', column: 'number' } + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) } } ] } ], - location: { file: 'a.test.ts', line: 'number', column: 'number' } + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) } } ], location: { file: 'a.test.ts', - line: 'number', - column: 'number' + line: expect.any(Number), + column: expect.any(Number) } }, { @@ -804,10 +809,10 @@ test('should nest steps based on zones', async ({ runInlineTest }) => { { title: 'in afterEach', category: 'test.step', - location: { file: 'a.test.ts', line: 'number', column: 'number' } + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) } } ], - location: { file: 'a.test.ts', line: 'number', column: 'number' } + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) } }, { category: 'fixture', @@ -824,10 +829,10 @@ test('should nest steps based on zones', async ({ runInlineTest }) => { { title: 'in afterAll', category: 'test.step', - location: { file: 'a.test.ts', line: 'number', column: 'number' } + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) } } ], - location: { file: 'a.test.ts', line: 'number', column: 'number' } + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) } }, ] } @@ -873,9 +878,9 @@ test('should not mark page.close as failed when page.click fails', async ({ runI category: 'hook', title: 'beforeAll hook', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, steps: [ { @@ -889,9 +894,9 @@ test('should not mark page.close as failed when page.click fails', async ({ runI category: 'pw:api', title: 'browser.newPage', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, }, ], @@ -902,20 +907,20 @@ test('should not mark page.close as failed when page.click fails', async ({ runI category: 'pw:api', title: 'page.setContent', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, }, { category: 'pw:api', title: 'page.click(div)', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, - error: '', + error: expect.any(String), }, { @@ -926,18 +931,18 @@ test('should not mark page.close as failed when page.click fails', async ({ runI category: 'hook', title: 'afterAll hook', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, steps: [ { category: 'pw:api', title: 'page.close', location: { - column: 'number', + column: expect.any(Number), file: 'a.test.ts', - line: 'number', + line: expect.any(Number), }, }, ], @@ -1003,17 +1008,17 @@ test('should nest page.continue inside page.goto steps', async ({ runInlineTest { title: 'page.route', category: 'pw:api', - location: { file: 'a.test.ts', line: 'number', column: 'number' }, + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) }, }, { title: 'page.goto(http://localhost:1234)', category: 'pw:api', - location: { file: 'a.test.ts', line: 'number', column: 'number' }, + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) }, steps: [ { title: 'route.fulfill', category: 'pw:api', - location: { file: 'a.test.ts', line: 'number', column: 'number' }, + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) }, }, ] }, @@ -1059,23 +1064,23 @@ test('should not propagate errors from within toPass', async ({ runInlineTest }) { title: 'expect.toPass', category: 'expect', - location: { file: 'a.test.ts', line: 'number', column: 'number' }, + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) }, steps: [ { category: 'expect', - error: '', - location: { file: 'a.test.ts', line: 'number', column: 'number' }, + error: expect.any(String), + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) }, title: 'expect.toBe', }, { category: 'expect', - error: '', - location: { file: 'a.test.ts', line: 'number', column: 'number' }, + error: expect.any(String), + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) }, title: 'expect.toBe', }, { category: 'expect', - location: { file: 'a.test.ts', line: 'number', column: 'number' }, + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) }, title: 'expect.toBe', }, ], @@ -1111,13 +1116,13 @@ test('should show final toPass error', async ({ runInlineTest }) => { { title: 'expect.toPass', category: 'expect', - error: '', - location: { file: 'a.test.ts', line: 'number', column: 'number' }, + error: expect.any(String), + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) }, steps: [ { category: 'expect', - error: '', - location: { file: 'a.test.ts', line: 'number', column: 'number' }, + error: expect.any(String), + location: { file: 'a.test.ts', line: expect.any(Number), column: expect.any(Number) }, title: 'expect.toBe', }, ], @@ -1161,20 +1166,20 @@ test('should propagate nested soft errors', async ({ runInlineTest }) => { { category: 'test.step', title: 'first outer', - error: '', - location: { column: 'number', file: 'a.test.ts', line: 'number' }, + error: expect.any(String), + location: { column: expect.any(Number), file: 'a.test.ts', line: expect.any(Number) }, steps: [ { category: 'test.step', title: 'first inner', - error: '', - location: { column: 'number', file: 'a.test.ts', line: 'number' }, + error: expect.any(String), + location: { column: expect.any(Number), file: 'a.test.ts', line: expect.any(Number) }, steps: [ { category: 'expect', title: 'expect.soft.toBe', - error: '', - location: { column: 'number', file: 'a.test.ts', line: 'number' }, + error: expect.any(String), + location: { column: expect.any(Number), file: 'a.test.ts', line: expect.any(Number) }, }, ], }, @@ -1183,20 +1188,20 @@ test('should propagate nested soft errors', async ({ runInlineTest }) => { { category: 'test.step', title: 'second outer', - error: '', - location: { column: 'number', file: 'a.test.ts', line: 'number' }, + error: expect.any(String), + location: { column: expect.any(Number), file: 'a.test.ts', line: expect.any(Number) }, steps: [ { category: 'test.step', title: 'second inner', - error: '', - location: { column: 'number', file: 'a.test.ts', line: 'number' }, + error: expect.any(String), + location: { column: expect.any(Number), file: 'a.test.ts', line: expect.any(Number) }, steps: [ { category: 'expect', title: 'expect.toBe', - error: '', - location: { column: 'number', file: 'a.test.ts', line: 'number' }, + error: expect.any(String), + location: { column: expect.any(Number), file: 'a.test.ts', line: expect.any(Number) }, }, ], }, @@ -1244,18 +1249,18 @@ test('should not propagate nested hard errors', async ({ runInlineTest }) => { { category: 'test.step', title: 'first outer', - location: { column: 'number', file: 'a.test.ts', line: 'number' }, + location: { column: expect.any(Number), file: 'a.test.ts', line: expect.any(Number) }, steps: [ { category: 'test.step', title: 'first inner', - location: { column: 'number', file: 'a.test.ts', line: 'number' }, + location: { column: expect.any(Number), file: 'a.test.ts', line: expect.any(Number) }, steps: [ { category: 'expect', title: 'expect.toBe', - error: '', - location: { column: 'number', file: 'a.test.ts', line: 'number' }, + error: expect.any(String), + location: { column: expect.any(Number), file: 'a.test.ts', line: expect.any(Number) }, }, ], }, @@ -1264,20 +1269,20 @@ test('should not propagate nested hard errors', async ({ runInlineTest }) => { { category: 'test.step', title: 'second outer', - error: '', - location: { column: 'number', file: 'a.test.ts', line: 'number' }, + error: expect.any(String), + location: { column: expect.any(Number), file: 'a.test.ts', line: expect.any(Number) }, steps: [ { category: 'test.step', title: 'second inner', - error: '', - location: { column: 'number', file: 'a.test.ts', line: 'number' }, + error: expect.any(String), + location: { column: expect.any(Number), file: 'a.test.ts', line: expect.any(Number) }, steps: [ { category: 'expect', title: 'expect.toBe', - error: '', - location: { column: 'number', file: 'a.test.ts', line: 'number' }, + error: expect.any(String), + location: { column: expect.any(Number), file: 'a.test.ts', line: expect.any(Number) }, }, ], }, @@ -1289,3 +1294,94 @@ test('should not propagate nested hard errors', async ({ runInlineTest }) => { }, ]); }); + +test('should step w/o box', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'reporter.ts': stepHierarchyReporter, + 'playwright.config.ts': `module.exports = { reporter: './reporter', };`, + 'a.test.ts': + ` /*1*/ import { test, expect } from '@playwright/test'; + /*2*/ test('fail', async () => { + /*3*/ await test.step('boxed step', async () => { + /*4*/ expect(1).toBe(2); + /*5*/ }); + /*6*/ }); + ` + }, { reporter: '' }); + + expect(result.exitCode).toBe(1); + const objects = result.outputLines.map(line => JSON.parse(line)); + expect(objects).toEqual([ + { + category: 'hook', + title: 'Before Hooks', + }, + { + category: 'test.step', + error: expect.stringContaining('a.test.ts:4:27'), + location: { + column: 26, + file: 'a.test.ts', + line: 3, + }, + steps: [ + { + category: 'expect', + error: expect.stringContaining('a.test.ts:4:27'), + location: { + column: 27, + file: 'a.test.ts', + line: 4, + }, + title: 'expect.toBe', + }, + ], + title: 'boxed step', + }, + { + category: 'hook', + title: 'After Hooks', + }, + ]); +}); + +test('should step w/ box', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'reporter.ts': stepHierarchyReporter, + 'playwright.config.ts': `module.exports = { reporter: './reporter', };`, + 'a.test.ts': + ` /*1*/ import { test, expect } from '@playwright/test'; + /*2*/ test('fail', async () => { + /*3*/ const helper = async () => { + /*4*/ await test.step('boxed step', async () => { + /*5*/ await expect(page.locator('body')).toHaveText('Good page', { timeout: 1 }); + /*6*/ }, { box: 'self' }); + /*7*/ }; + /*8*/ await helper(); + /*9*/ }); + ` + }, { reporter: '' }); + + expect(result.exitCode).toBe(1); + const objects = result.outputLines.map(line => JSON.parse(line)); + expect(objects).toEqual([ + { + category: 'hook', + title: 'Before Hooks', + }, + { + category: 'test.step', + error: expect.not.stringMatching(/a.test.ts:[^8]/), + location: { + column: 21, + file: 'a.test.ts', + line: 8, + }, + title: 'boxed step', + }, + { + category: 'hook', + title: 'After Hooks', + }, + ]); +}); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index f95b9b665c..e38fba16a2 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -157,7 +157,7 @@ export interface TestType Promise | any): void; afterAll(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | any): void; use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void; - step(title: string, body: () => T | Promise): Promise; + step(title: string, body: () => T | Promise, options?: { box?: boolean }): Promise; expect: Expect<{}>; extend(fixtures: Fixtures): TestType; info(): TestInfo;