From a1451c75f8f0fdc471f2921feb5a9cb38d28d23d Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 31 Jan 2025 15:45:57 -0800 Subject: [PATCH] feat: conditional step.skip() (#34578) --- docs/src/test-api/class-test.md | 2 +- docs/src/test-api/class-teststepinfo.md | 39 ++++++++++++ docs/src/test-reporter-api/class-teststep.md | 8 +++ packages/playwright/src/common/ipc.ts | 1 + packages/playwright/src/common/testType.ts | 11 +--- .../playwright/src/isomorphic/teleReceiver.ts | 5 ++ packages/playwright/src/reporters/html.ts | 9 ++- .../playwright/src/reporters/teleEmitter.ts | 1 + packages/playwright/src/runner/dispatcher.ts | 2 + packages/playwright/src/worker/testInfo.ts | 37 ++++++++++- packages/playwright/src/worker/testTracing.ts | 5 +- packages/playwright/types/test.d.ts | 37 ++++++++++- packages/playwright/types/testReporter.d.ts | 15 +++++ .../trace-viewer/src/sw/traceModernizer.ts | 1 + packages/trace-viewer/src/ui/actionList.tsx | 2 +- packages/trace-viewer/src/ui/modelUtil.ts | 2 + packages/trace/src/trace.ts | 6 ++ tests/playwright-test/reporter-html.spec.ts | 27 +++++++- tests/playwright-test/test-step.spec.ts | 62 ++++++++++++++++--- utils/generate_types/overrides-test.d.ts | 4 +- 20 files changed, 244 insertions(+), 32 deletions(-) create mode 100644 docs/src/test-api/class-teststepinfo.md diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md index 77c0c5028c..a719ee6bd5 100644 --- a/docs/src/test-api/class-test.md +++ b/docs/src/test-api/class-test.md @@ -1751,7 +1751,7 @@ Step name. ### param: Test.step.body * since: v1.10 -- `body` <[function]\(\):[Promise]<[any]>> +- `body` <[function]\([TestStepInfo]\):[Promise]<[any]>> Step body. diff --git a/docs/src/test-api/class-teststepinfo.md b/docs/src/test-api/class-teststepinfo.md new file mode 100644 index 0000000000..a453d212c8 --- /dev/null +++ b/docs/src/test-api/class-teststepinfo.md @@ -0,0 +1,39 @@ +# class: TestStepInfo +* since: v1.51 +* langs: js + +`TestStepInfo` contains information about currently running test step. It is passed as an argument to the step function. `TestStepInfo` provides utilities to control test step execution. + +```js +import { test, expect } from '@playwright/test'; + +test('basic test', async ({ page, browserName }, TestStepInfo) => { + await test.step('check some behavior', async step => { + await step.skip(browserName === 'webkit', 'The feature is not available in WebKit'); + // ... rest of the step code + await page.check('input'); + }); +}); +``` + +## method: TestStepInfo.skip#1 +* since: v1.51 + +Unconditionally skip the currently running step. Test step is immediately aborted. This is similar to [`method: Test.step.skip`]. + +## method: TestStepInfo.skip#2 +* since: v1.51 + +Conditionally skips the currently running step with an optional description. This is similar to [`method: Test.step.skip`]. + +### param: TestStepInfo.skip#2.condition +* since: v1.51 +- `condition` <[boolean]> + +A skip condition. Test step is skipped when the condition is `true`. + +### param: TestStepInfo.skip#2.description +* since: v1.51 +- `description` ?<[string]> + +Optional description that will be reflected in a test report. diff --git a/docs/src/test-reporter-api/class-teststep.md b/docs/src/test-reporter-api/class-teststep.md index ef16e4849a..1272eb546f 100644 --- a/docs/src/test-reporter-api/class-teststep.md +++ b/docs/src/test-reporter-api/class-teststep.md @@ -50,6 +50,14 @@ Start time of this particular test step. List of steps inside this step. +## property: TestStep.annotations +* since: v1.51 +- type: <[Array]<[Object]>> + - `type` <[string]> Annotation type, for example `'skip'`. + - `description` ?<[string]> Optional description. + +The list of annotations applicable to the current test step. + ## property: TestStep.attachments * since: v1.50 - type: <[Array]<[Object]>> diff --git a/packages/playwright/src/common/ipc.ts b/packages/playwright/src/common/ipc.ts index 76ee996216..2e085f5b78 100644 --- a/packages/playwright/src/common/ipc.ts +++ b/packages/playwright/src/common/ipc.ts @@ -109,6 +109,7 @@ export type StepEndPayload = { wallTime: number; // milliseconds since unix epoch error?: TestInfoErrorImpl; suggestedRebaseline?: string; + annotations: { type: string, description?: string }[]; }; export type TestEntry = { diff --git a/packages/playwright/src/common/testType.ts b/packages/playwright/src/common/testType.ts index 58d813a99e..71761b33e1 100644 --- a/packages/playwright/src/common/testType.ts +++ b/packages/playwright/src/common/testType.ts @@ -19,7 +19,7 @@ import { currentlyLoadingFileSuite, currentTestInfo, setCurrentlyLoadingFileSuit import { TestCase, Suite } from './test'; import { wrapFunctionWithLocation } from '../transform/transform'; import type { FixturesWithLocation } from './config'; -import type { Fixtures, TestType, TestDetails } from '../../types/test'; +import type { Fixtures, TestType, TestDetails, TestStepInfo } from '../../types/test'; import type { Location } from '../../types/testReporter'; import { getPackageManagerExecCommand, monotonicTime, raceAgainstDeadline, zones } from 'playwright-core/lib/utils'; import { errors } from 'playwright-core'; @@ -258,22 +258,17 @@ export class TestTypeImpl { suite._use.push({ fixtures, location }); } - async _step(expectation: 'pass'|'skip', title: string, body: () => T | Promise, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise { + async _step(expectation: 'pass'|'skip', title: string, body: (step: TestStepInfo) => T | Promise, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise { const testInfo = currentTestInfo(); if (!testInfo) throw new Error(`test.step() can only be called from a test`); - if (expectation === 'skip') { - const step = testInfo._addStep({ category: 'test.step.skip', title, location: options.location, box: options.box }); - step.complete({}); - return undefined as T; - } const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box }); return await zones.run('stepZone', step, async () => { try { let result: Awaited>> | undefined = undefined; result = await raceAgainstDeadline(async () => { try { - return await body(); + return await step.info._runStepBody(expectation === 'skip', body); } catch (e) { // If the step timed out, the test fixtures will tear down, which in turn // will abort unfinished actions in the step body. Record such errors here. diff --git a/packages/playwright/src/isomorphic/teleReceiver.ts b/packages/playwright/src/isomorphic/teleReceiver.ts index 1d41b793cd..1eb83df623 100644 --- a/packages/playwright/src/isomorphic/teleReceiver.ts +++ b/packages/playwright/src/isomorphic/teleReceiver.ts @@ -109,6 +109,7 @@ export type JsonTestStepEnd = { duration: number; error?: reporterTypes.TestError; attachments?: number[]; // index of JsonTestResultEnd.attachments + annotations?: Annotation[]; }; export type JsonFullResult = { @@ -546,6 +547,10 @@ class TeleTestStep implements reporterTypes.TestStep { get attachments() { return this._endPayload?.attachments?.map(index => this._result.attachments[index]) ?? []; } + + get annotations() { + return this._endPayload?.annotations ?? []; + } } export class TeleTestResult implements reporterTypes.TestResult { diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 1fd7695293..e258f22798 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -517,9 +517,12 @@ class HtmlBuilder { private _createTestStep(dedupedStep: DedupedStep, result: api.TestResult): TestStep { const { step, duration, count } = dedupedStep; - const skipped = dedupedStep.step.category === 'test.step.skip'; + const skipped = dedupedStep.step.annotations?.find(a => a.type === 'skip'); + let title = step.title; + if (skipped) + title = `${title} (skipped${skipped.description ? ': ' + skipped.description : ''})`; const testStep: TestStep = { - title: step.title, + title, startTime: step.startTime.toISOString(), duration, steps: dedupeSteps(step.steps).map(s => this._createTestStep(s, result)), @@ -532,7 +535,7 @@ class HtmlBuilder { location: this._relativeLocation(step.location), error: step.error?.message, count, - skipped + skipped: !!skipped, }; if (step.location) this._stepsInFile.set(step.location.file, testStep); diff --git a/packages/playwright/src/reporters/teleEmitter.ts b/packages/playwright/src/reporters/teleEmitter.ts index 0ec92ae9ac..eadd2b05a5 100644 --- a/packages/playwright/src/reporters/teleEmitter.ts +++ b/packages/playwright/src/reporters/teleEmitter.ts @@ -257,6 +257,7 @@ export class TeleReporterEmitter implements ReporterV2 { duration: step.duration, error: step.error, attachments: step.attachments.map(a => result.attachments.indexOf(a)), + annotations: step.annotations.length ? step.annotations : undefined, }; } diff --git a/packages/playwright/src/runner/dispatcher.ts b/packages/playwright/src/runner/dispatcher.ts index 534fe7eb4a..9f6367c106 100644 --- a/packages/playwright/src/runner/dispatcher.ts +++ b/packages/playwright/src/runner/dispatcher.ts @@ -321,6 +321,7 @@ class JobDispatcher { duration: -1, steps: [], attachments: [], + annotations: [], location: params.location, }; steps.set(params.stepId, step); @@ -345,6 +346,7 @@ class JobDispatcher { step.error = params.error; if (params.suggestedRebaseline) addSuggestedRebaseline(step.location!, params.suggestedRebaseline); + step.annotations = params.annotations; steps.delete(params.stepId); this._reporter.onStepEnd?.(test, result, step); } diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 3efd3b3750..09ce7a0ddd 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -17,7 +17,7 @@ import fs from 'fs'; import path from 'path'; import { captureRawStack, monotonicTime, zones, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils'; -import type { TestInfo, TestStatus, FullProject } from '../../types/test'; +import type { TestInfo, TestStatus, FullProject, TestStepInfo } from '../../types/test'; import type { AttachmentPayload, StepBeginPayload, StepEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc'; import type { TestCase } from '../common/test'; import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager'; @@ -31,6 +31,7 @@ import { testInfoError } from './util'; export interface TestStepInternal { complete(result: { error?: Error | unknown, suggestedRebaseline?: string }): void; + info: TestStepInfoImpl attachmentIndices: number[]; stepId: string; title: string; @@ -244,7 +245,7 @@ export class TestInfoImpl implements TestInfo { ?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent. } - _addStep(data: Omit, parentStep?: TestStepInternal): TestStepInternal { + _addStep(data: Omit, parentStep?: TestStepInternal): TestStepInternal { const stepId = `${data.category}@${++this._lastStepId}`; if (data.isStage) { @@ -269,6 +270,7 @@ export class TestInfoImpl implements TestInfo { ...data, steps: [], attachmentIndices, + info: new TestStepInfoImpl(), complete: result => { if (step.endWallTime) return; @@ -302,11 +304,12 @@ export class TestInfoImpl implements TestInfo { wallTime: step.endWallTime, error: step.error, suggestedRebaseline: result.suggestedRebaseline, + annotations: step.info.annotations, }; this._onStepEnd(payload); const errorForTrace = step.error ? { name: '', message: step.error.message || '', stack: step.error.stack } : undefined; const attachments = attachmentIndices.map(i => this.attachments[i]); - this._tracing.appendAfterActionForStep(stepId, errorForTrace, attachments); + this._tracing.appendAfterActionForStep(stepId, errorForTrace, attachments, step.info.annotations); } }; const parentStepList = parentStep ? parentStep.steps : this._steps; @@ -504,6 +507,34 @@ export class TestInfoImpl implements TestInfo { } } +export class TestStepInfoImpl implements TestStepInfo { + annotations: Annotation[] = []; + + async _runStepBody(skip: boolean, body: (step: TestStepInfo) => T | Promise) { + if (skip) { + this.annotations.push({ type: 'skip' }); + return undefined as T; + } + try { + return await body(this); + } catch (e) { + if (e instanceof SkipError) + return undefined as T; + throw e; + } + } + + skip(...args: unknown[]) { + // skip(); + // skip(condition: boolean, description: string); + if (args.length > 0 && !args[0]) + return; + const description = args[1] as (string|undefined); + this.annotations.push({ type: 'skip', description }); + throw new SkipError(description); + } +} + export class SkipError extends Error { } diff --git a/packages/playwright/src/worker/testTracing.ts b/packages/playwright/src/worker/testTracing.ts index 7b83fa48eb..15bb17db06 100644 --- a/packages/playwright/src/worker/testTracing.ts +++ b/packages/playwright/src/worker/testTracing.ts @@ -252,19 +252,20 @@ export class TestTracing { parentId, startTime: monotonicTime(), class: 'Test', - method: category, + method: 'step', apiName, params: Object.fromEntries(Object.entries(params || {}).map(([name, value]) => [name, generatePreview(value)])), stack, }); } - appendAfterActionForStep(callId: string, error?: SerializedError['error'], attachments: Attachment[] = []) { + appendAfterActionForStep(callId: string, error?: SerializedError['error'], attachments: Attachment[] = [], annotations?: trace.AfterActionTraceEventAnnotation[]) { this._appendTraceEvent({ type: 'after', callId, endTime: monotonicTime(), attachments: serializeAttachments(attachments), + annotations, error, }); } diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index b1d38068e8..95eb71d899 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -5811,7 +5811,7 @@ export interface TestType { * @param body Step body. * @param options */ - (title: string, body: () => T | Promise, options?: { box?: boolean, location?: Location, timeout?: number }): Promise; + (title: string, body: (step: TestStepInfo) => T | Promise, options?: { box?: boolean, location?: Location, timeout?: number }): Promise; /** * Mark a test step as "skip" to temporarily disable its execution, useful for steps that are currently failing and * planned for a near-term fix. Playwright will not run the step. @@ -5835,7 +5835,7 @@ export interface TestType { * @param body Step body. * @param options */ - skip(title: string, body: () => any | Promise, options?: { box?: boolean, location?: Location, timeout?: number }): Promise; + skip(title: string, body: (step: TestStepInfo) => any | Promise, options?: { box?: boolean, location?: Location, timeout?: number }): Promise; } /** * `expect` function can be used to create test assertions. Read more about [test assertions](https://playwright.dev/docs/test-assertions). @@ -9553,6 +9553,39 @@ export interface TestInfoError { value?: string; } +/** + * `TestStepInfo` contains information about currently running test step. It is passed as an argument to the step + * function. `TestStepInfo` provides utilities to control test step execution. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test('basic test', async ({ page, browserName }, TestStepInfo) => { + * await test.step('check some behavior', async step => { + * await step.skip(browserName === 'webkit', 'The feature is not available in WebKit'); + * // ... rest of the step code + * await page.check('input'); + * }); + * }); + * ``` + * + */ +export interface TestStepInfo { + /** + * Unconditionally skip the currently running step. Test step is immediately aborted. This is similar to + * [test.step.skip(title, body[, options])](https://playwright.dev/docs/api/class-test#test-step-skip). + */ + skip(): void; + + /** + * Conditionally skips the currently running step with an optional description. This is similar to + * [test.step.skip(title, body[, options])](https://playwright.dev/docs/api/class-test#test-step-skip). + * @param condition A skip condition. Test step is skipped when the condition is `true`. + * @param description Optional description that will be reflected in a test report. + */ + skip(condition: boolean, description?: string): void; +} + /** * `WorkerInfo` contains information about the worker that is running tests and is available to worker-scoped * fixtures. `WorkerInfo` is a subset of [TestInfo](https://playwright.dev/docs/api/class-testinfo) that is available diff --git a/packages/playwright/types/testReporter.d.ts b/packages/playwright/types/testReporter.d.ts index 3f3a43984e..9662faee73 100644 --- a/packages/playwright/types/testReporter.d.ts +++ b/packages/playwright/types/testReporter.d.ts @@ -691,6 +691,21 @@ export interface TestStep { */ titlePath(): Array; + /** + * The list of annotations applicable to the current test step. + */ + annotations: Array<{ + /** + * Annotation type, for example `'skip'`. + */ + type: string; + + /** + * Optional description. + */ + description?: string; + }>; + /** * The list of files or buffers attached in the step execution through * [testInfo.attach(name[, options])](https://playwright.dev/docs/api/class-testinfo#test-info-attach). diff --git a/packages/trace-viewer/src/sw/traceModernizer.ts b/packages/trace-viewer/src/sw/traceModernizer.ts index 80f98762db..6b4b268cb2 100644 --- a/packages/trace-viewer/src/sw/traceModernizer.ts +++ b/packages/trace-viewer/src/sw/traceModernizer.ts @@ -126,6 +126,7 @@ export class TraceModernizer { existing!.result = event.result; existing!.error = event.error; existing!.attachments = event.attachments; + existing!.annotations = event.annotations; if (event.point) existing!.point = event.point; break; diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx index 2c6932bd45..4f3a8128e8 100644 --- a/packages/trace-viewer/src/ui/actionList.tsx +++ b/packages/trace-viewer/src/ui/actionList.tsx @@ -120,7 +120,7 @@ export const renderAction = ( const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript'); - const isSkipped = action.class === 'Test' && action.method === 'test.step.skip'; + const isSkipped = action.class === 'Test' && action.method === 'step' && action.annotations?.some(a => a.type === 'skip'); let time: string = ''; if (action.endTime) time = msToString(action.endTime - action.startTime); diff --git a/packages/trace-viewer/src/ui/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts index 8badcbd87e..01af841748 100644 --- a/packages/trace-viewer/src/ui/modelUtil.ts +++ b/packages/trace-viewer/src/ui/modelUtil.ts @@ -258,6 +258,8 @@ function mergeActionsAndUpdateTimingSameTrace(contexts: ContextEntry[]): ActionT existing.error = action.error; if (action.attachments) existing.attachments = action.attachments; + if (action.annotations) + existing.annotations = action.annotations; if (action.parentId) existing.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId; // For the events that are present in the test runner context, always take diff --git a/packages/trace/src/trace.ts b/packages/trace/src/trace.ts index 81fc4ab428..6c9c7be1a0 100644 --- a/packages/trace/src/trace.ts +++ b/packages/trace/src/trace.ts @@ -86,6 +86,11 @@ export type AfterActionTraceEventAttachment = { base64?: string; }; +export type AfterActionTraceEventAnnotation = { + type: string, + description?: string +}; + export type AfterActionTraceEvent = { type: 'after', callId: string; @@ -93,6 +98,7 @@ export type AfterActionTraceEvent = { afterSnapshot?: string; error?: SerializedError['error']; attachments?: AfterActionTraceEventAttachment[]; + annotations?: AfterActionTraceEventAnnotation[]; result?: any; point?: Point; }; diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index a4f41874aa..b05ff8ad55 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -780,10 +780,35 @@ for (const useIntermediateMergeReport of [true, false] as const) { await showReport(); await page.click('text=example'); - await page.click('text=skipped step title'); + await page.click('text=skipped step title (skipped)'); await expect(page.getByTestId('test-snippet')).toContainText(`await test.step.skip('skipped step title', async () => {`); }); + test('step title should inlclude skipped step description', async ({ runInlineTest, page, showReport }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + export default { testDir: './tests' }; + `, + 'tests/a.test.ts': ` + import { test, expect } from '@playwright/test'; + + test('example', async ({}) => { + await test.step('step title', async (step) => { + expect(1).toBe(1); + step.skip(true, 'conditional step.skip'); + }); + }); + `, + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + + await showReport(); + await page.click('text=example'); + await page.click('text=step title (skipped: conditional step.skip)'); + await expect(page.getByTestId('test-snippet')).toContainText(`await test.step('step title', async (step) => {`); + }); + test('should render annotations', async ({ runInlineTest, page, showReport }) => { const result = await runInlineTest({ 'playwright.config.js': ` diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index 7d509fda25..be72e51dd1 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -75,7 +75,9 @@ export default class MyReporter implements Reporter { let location = ''; if (step.location) location = formatLocation(step.location); - console.log(formatPrefix(step.category) + indent + step.title + location); + const skip = step.annotations?.find(a => a.type === 'skip'); + const skipped = skip?.description ? ' (skipped: ' + skip.description + ')' : skip ? ' (skipped)' : ''; + console.log(formatPrefix(step.category) + indent + step.title + location + skipped); if (step.error) { const errorLocation = this.printErrorLocation ? formatLocation(step.error.location) : ''; console.log(formatPrefix(step.category) + indent + '↪ error: ' + this.trimError(step.error.message!) + errorLocation); @@ -362,18 +364,16 @@ hook |Worker Cleanup `); }); -test('should not pass arguments and return value from step', async ({ runInlineTest }) => { +test('should not pass return value from step', async ({ runInlineTest }) => { const result = await runInlineTest({ 'a.test.ts': ` import { test, expect } from '@playwright/test'; test('steps with return values', async ({ page }) => { - const v1 = await test.step('my step', (...args) => { - expect(args.length).toBe(0); + const v1 = await test.step('my step', () => { return 10; }); console.log('v1 = ' + v1); - const v2 = await test.step('my step', async (...args) => { - expect(args.length).toBe(0); + const v2 = await test.step('my step', async () => { return new Promise(f => setTimeout(() => f(v1 + 10), 100)); }); console.log('v2 = ' + v2); @@ -1549,9 +1549,9 @@ test('test.step.skip should work', async ({ runInlineTest }) => { expect(result.report.stats.unexpected).toBe(0); expect(stripAnsi(result.output)).toBe(` hook |Before Hooks -test.step.skip|outer step 1 @ a.test.ts:4 +test.step |outer step 1 @ a.test.ts:4 (skipped) test.step |outer step 2 @ a.test.ts:11 -test.step.skip| inner step 2.1 @ a.test.ts:12 +test.step | inner step 2.1 @ a.test.ts:12 (skipped) test.step | inner step 2.2 @ a.test.ts:13 expect | expect.toBe @ a.test.ts:14 hook |After Hooks @@ -1581,12 +1581,56 @@ test('skip test.step.skip body', async ({ runInlineTest }) => { expect(stripAnsi(result.output)).toBe(` hook |Before Hooks test.step |outer step 2 @ a.test.ts:5 -test.step.skip| inner step 2 @ a.test.ts:6 +test.step | inner step 2 @ a.test.ts:6 (skipped) expect |expect.toBe @ a.test.ts:10 hook |After Hooks `); }); +test('step.skip should work at runtime', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'reporter.ts': stepIndentReporter, + 'playwright.config.ts': `module.exports = { reporter: './reporter' };`, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('test', async ({ }) => { + await test.step('outer step 1', async () => { + await test.step('inner step 1.1', async (step) => { + step.skip(); + }); + await test.step('inner step 1.2', async (step) => { + step.skip(true, 'condition is true'); + }); + await test.step('inner step 1.3', async () => {}); + }); + await test.step('outer step 2', async () => { + await test.step.skip('inner step 2.1', async () => {}); + await test.step('inner step 2.2', async () => { + expect(1).toBe(1); + }); + }); + }); + ` + }, { reporter: '' }); + + expect(result.exitCode).toBe(0); + expect(result.report.stats.expected).toBe(1); + expect(result.report.stats.unexpected).toBe(0); + expect(stripAnsi(result.output)).toBe(` +hook |Before Hooks +test.step |outer step 1 @ a.test.ts:4 +test.step | inner step 1.1 @ a.test.ts:5 (skipped) +test.step | inner step 1.2 @ a.test.ts:8 (skipped: condition is true) +test.step | inner step 1.3 @ a.test.ts:11 +test.step |outer step 2 @ a.test.ts:13 +test.step | inner step 2.1 @ a.test.ts:14 (skipped) +test.step | inner step 2.2 @ a.test.ts:15 +expect | expect.toBe @ a.test.ts:16 +hook |After Hooks +`); +}); + + test('show api calls inside expects', async ({ runInlineTest }) => { const result = await runInlineTest({ 'reporter.ts': stepIndentReporter, diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 1bc980b42d..04121cb281 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -163,8 +163,8 @@ export interface TestType { afterAll(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | any): void; use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void; step: { - (title: string, body: () => T | Promise, options?: { box?: boolean, location?: Location, timeout?: number }): Promise; - skip(title: string, body: () => any | Promise, options?: { box?: boolean, location?: Location, timeout?: number }): Promise; + (title: string, body: (step: TestStepInfo) => T | Promise, options?: { box?: boolean, location?: Location, timeout?: number }): Promise; + skip(title: string, body: (step: TestStepInfo) => any | Promise, options?: { box?: boolean, location?: Location, timeout?: number }): Promise; } expect: Expect<{}>; extend(fixtures: Fixtures): TestType;