feat: conditional step.skip() (#34578)

This commit is contained in:
Yury Semikhatsky 2025-01-31 15:45:57 -08:00 committed by GitHub
parent da12af24c2
commit a1451c75f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 244 additions and 32 deletions

View File

@ -1751,7 +1751,7 @@ Step name.
### param: Test.step.body ### param: Test.step.body
* since: v1.10 * since: v1.10
- `body` <[function]\(\):[Promise]<[any]>> - `body` <[function]\([TestStepInfo]\):[Promise]<[any]>>
Step body. Step body.

View File

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

View File

@ -50,6 +50,14 @@ Start time of this particular test step.
List of steps inside this 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 ## property: TestStep.attachments
* since: v1.50 * since: v1.50
- type: <[Array]<[Object]>> - type: <[Array]<[Object]>>

View File

@ -109,6 +109,7 @@ export type StepEndPayload = {
wallTime: number; // milliseconds since unix epoch wallTime: number; // milliseconds since unix epoch
error?: TestInfoErrorImpl; error?: TestInfoErrorImpl;
suggestedRebaseline?: string; suggestedRebaseline?: string;
annotations: { type: string, description?: string }[];
}; };
export type TestEntry = { export type TestEntry = {

View File

@ -19,7 +19,7 @@ import { currentlyLoadingFileSuite, currentTestInfo, setCurrentlyLoadingFileSuit
import { TestCase, Suite } from './test'; import { TestCase, Suite } from './test';
import { wrapFunctionWithLocation } from '../transform/transform'; import { wrapFunctionWithLocation } from '../transform/transform';
import type { FixturesWithLocation } from './config'; 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 type { Location } from '../../types/testReporter';
import { getPackageManagerExecCommand, monotonicTime, raceAgainstDeadline, zones } from 'playwright-core/lib/utils'; import { getPackageManagerExecCommand, monotonicTime, raceAgainstDeadline, zones } from 'playwright-core/lib/utils';
import { errors } from 'playwright-core'; import { errors } from 'playwright-core';
@ -258,22 +258,17 @@ export class TestTypeImpl {
suite._use.push({ fixtures, location }); suite._use.push({ fixtures, location });
} }
async _step<T>(expectation: 'pass'|'skip', title: string, body: () => T | Promise<T>, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise<T> { async _step<T>(expectation: 'pass'|'skip', title: string, body: (step: TestStepInfo) => T | Promise<T>, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise<T> {
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (!testInfo) if (!testInfo)
throw new Error(`test.step() can only be called from a test`); 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 }); const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box });
return await zones.run('stepZone', step, async () => { return await zones.run('stepZone', step, async () => {
try { try {
let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined; let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;
result = await raceAgainstDeadline(async () => { result = await raceAgainstDeadline(async () => {
try { try {
return await body(); return await step.info._runStepBody(expectation === 'skip', body);
} catch (e) { } catch (e) {
// If the step timed out, the test fixtures will tear down, which in turn // 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. // will abort unfinished actions in the step body. Record such errors here.

View File

@ -109,6 +109,7 @@ export type JsonTestStepEnd = {
duration: number; duration: number;
error?: reporterTypes.TestError; error?: reporterTypes.TestError;
attachments?: number[]; // index of JsonTestResultEnd.attachments attachments?: number[]; // index of JsonTestResultEnd.attachments
annotations?: Annotation[];
}; };
export type JsonFullResult = { export type JsonFullResult = {
@ -546,6 +547,10 @@ class TeleTestStep implements reporterTypes.TestStep {
get attachments() { get attachments() {
return this._endPayload?.attachments?.map(index => this._result.attachments[index]) ?? []; return this._endPayload?.attachments?.map(index => this._result.attachments[index]) ?? [];
} }
get annotations() {
return this._endPayload?.annotations ?? [];
}
} }
export class TeleTestResult implements reporterTypes.TestResult { export class TeleTestResult implements reporterTypes.TestResult {

View File

@ -517,9 +517,12 @@ class HtmlBuilder {
private _createTestStep(dedupedStep: DedupedStep, result: api.TestResult): TestStep { private _createTestStep(dedupedStep: DedupedStep, result: api.TestResult): TestStep {
const { step, duration, count } = dedupedStep; 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 = { const testStep: TestStep = {
title: step.title, title,
startTime: step.startTime.toISOString(), startTime: step.startTime.toISOString(),
duration, duration,
steps: dedupeSteps(step.steps).map(s => this._createTestStep(s, result)), steps: dedupeSteps(step.steps).map(s => this._createTestStep(s, result)),
@ -532,7 +535,7 @@ class HtmlBuilder {
location: this._relativeLocation(step.location), location: this._relativeLocation(step.location),
error: step.error?.message, error: step.error?.message,
count, count,
skipped skipped: !!skipped,
}; };
if (step.location) if (step.location)
this._stepsInFile.set(step.location.file, testStep); this._stepsInFile.set(step.location.file, testStep);

View File

@ -257,6 +257,7 @@ export class TeleReporterEmitter implements ReporterV2 {
duration: step.duration, duration: step.duration,
error: step.error, error: step.error,
attachments: step.attachments.map(a => result.attachments.indexOf(a)), attachments: step.attachments.map(a => result.attachments.indexOf(a)),
annotations: step.annotations.length ? step.annotations : undefined,
}; };
} }

View File

@ -321,6 +321,7 @@ class JobDispatcher {
duration: -1, duration: -1,
steps: [], steps: [],
attachments: [], attachments: [],
annotations: [],
location: params.location, location: params.location,
}; };
steps.set(params.stepId, step); steps.set(params.stepId, step);
@ -345,6 +346,7 @@ class JobDispatcher {
step.error = params.error; step.error = params.error;
if (params.suggestedRebaseline) if (params.suggestedRebaseline)
addSuggestedRebaseline(step.location!, params.suggestedRebaseline); addSuggestedRebaseline(step.location!, params.suggestedRebaseline);
step.annotations = params.annotations;
steps.delete(params.stepId); steps.delete(params.stepId);
this._reporter.onStepEnd?.(test, result, step); this._reporter.onStepEnd?.(test, result, step);
} }

View File

@ -17,7 +17,7 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { captureRawStack, monotonicTime, zones, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils'; 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 { AttachmentPayload, StepBeginPayload, StepEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc';
import type { TestCase } from '../common/test'; import type { TestCase } from '../common/test';
import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager'; import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager';
@ -31,6 +31,7 @@ import { testInfoError } from './util';
export interface TestStepInternal { export interface TestStepInternal {
complete(result: { error?: Error | unknown, suggestedRebaseline?: string }): void; complete(result: { error?: Error | unknown, suggestedRebaseline?: string }): void;
info: TestStepInfoImpl
attachmentIndices: number[]; attachmentIndices: number[];
stepId: string; stepId: string;
title: 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. ?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent.
} }
_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps' | 'attachmentIndices'>, parentStep?: TestStepInternal): TestStepInternal { _addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps' | 'attachmentIndices' | 'info'>, parentStep?: TestStepInternal): TestStepInternal {
const stepId = `${data.category}@${++this._lastStepId}`; const stepId = `${data.category}@${++this._lastStepId}`;
if (data.isStage) { if (data.isStage) {
@ -269,6 +270,7 @@ export class TestInfoImpl implements TestInfo {
...data, ...data,
steps: [], steps: [],
attachmentIndices, attachmentIndices,
info: new TestStepInfoImpl(),
complete: result => { complete: result => {
if (step.endWallTime) if (step.endWallTime)
return; return;
@ -302,11 +304,12 @@ export class TestInfoImpl implements TestInfo {
wallTime: step.endWallTime, wallTime: step.endWallTime,
error: step.error, error: step.error,
suggestedRebaseline: result.suggestedRebaseline, suggestedRebaseline: result.suggestedRebaseline,
annotations: step.info.annotations,
}; };
this._onStepEnd(payload); this._onStepEnd(payload);
const errorForTrace = step.error ? { name: '', message: step.error.message || '', stack: step.error.stack } : undefined; const errorForTrace = step.error ? { name: '', message: step.error.message || '', stack: step.error.stack } : undefined;
const attachments = attachmentIndices.map(i => this.attachments[i]); 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; 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<T>(skip: boolean, body: (step: TestStepInfo) => T | Promise<T>) {
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 { export class SkipError extends Error {
} }

View File

@ -252,19 +252,20 @@ export class TestTracing {
parentId, parentId,
startTime: monotonicTime(), startTime: monotonicTime(),
class: 'Test', class: 'Test',
method: category, method: 'step',
apiName, apiName,
params: Object.fromEntries(Object.entries(params || {}).map(([name, value]) => [name, generatePreview(value)])), params: Object.fromEntries(Object.entries(params || {}).map(([name, value]) => [name, generatePreview(value)])),
stack, stack,
}); });
} }
appendAfterActionForStep(callId: string, error?: SerializedError['error'], attachments: Attachment[] = []) { appendAfterActionForStep(callId: string, error?: SerializedError['error'], attachments: Attachment[] = [], annotations?: trace.AfterActionTraceEventAnnotation[]) {
this._appendTraceEvent({ this._appendTraceEvent({
type: 'after', type: 'after',
callId, callId,
endTime: monotonicTime(), endTime: monotonicTime(),
attachments: serializeAttachments(attachments), attachments: serializeAttachments(attachments),
annotations,
error, error,
}); });
} }

View File

@ -5811,7 +5811,7 @@ export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
* @param body Step body. * @param body Step body.
* @param options * @param options
*/ */
<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<T>; <T>(title: string, body: (step: TestStepInfo) => T | Promise<T>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<T>;
/** /**
* Mark a test step as "skip" to temporarily disable its execution, useful for steps that are currently failing and * 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. * planned for a near-term fix. Playwright will not run the step.
@ -5835,7 +5835,7 @@ export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
* @param body Step body. * @param body Step body.
* @param options * @param options
*/ */
skip(title: string, body: () => any | Promise<any>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<void>; skip(title: string, body: (step: TestStepInfo) => any | Promise<any>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<void>;
} }
/** /**
* `expect` function can be used to create test assertions. Read more about [test assertions](https://playwright.dev/docs/test-assertions). * `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; 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 * `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 * fixtures. `WorkerInfo` is a subset of [TestInfo](https://playwright.dev/docs/api/class-testinfo) that is available

View File

@ -691,6 +691,21 @@ export interface TestStep {
*/ */
titlePath(): Array<string>; titlePath(): Array<string>;
/**
* 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 * 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). * [testInfo.attach(name[, options])](https://playwright.dev/docs/api/class-testinfo#test-info-attach).

View File

@ -126,6 +126,7 @@ export class TraceModernizer {
existing!.result = event.result; existing!.result = event.result;
existing!.error = event.error; existing!.error = event.error;
existing!.attachments = event.attachments; existing!.attachments = event.attachments;
existing!.annotations = event.annotations;
if (event.point) if (event.point)
existing!.point = event.point; existing!.point = event.point;
break; break;

View File

@ -120,7 +120,7 @@ export const renderAction = (
const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript'); 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 = ''; let time: string = '';
if (action.endTime) if (action.endTime)
time = msToString(action.endTime - action.startTime); time = msToString(action.endTime - action.startTime);

View File

@ -258,6 +258,8 @@ function mergeActionsAndUpdateTimingSameTrace(contexts: ContextEntry[]): ActionT
existing.error = action.error; existing.error = action.error;
if (action.attachments) if (action.attachments)
existing.attachments = action.attachments; existing.attachments = action.attachments;
if (action.annotations)
existing.annotations = action.annotations;
if (action.parentId) if (action.parentId)
existing.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId; existing.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId;
// For the events that are present in the test runner context, always take // For the events that are present in the test runner context, always take

View File

@ -86,6 +86,11 @@ export type AfterActionTraceEventAttachment = {
base64?: string; base64?: string;
}; };
export type AfterActionTraceEventAnnotation = {
type: string,
description?: string
};
export type AfterActionTraceEvent = { export type AfterActionTraceEvent = {
type: 'after', type: 'after',
callId: string; callId: string;
@ -93,6 +98,7 @@ export type AfterActionTraceEvent = {
afterSnapshot?: string; afterSnapshot?: string;
error?: SerializedError['error']; error?: SerializedError['error'];
attachments?: AfterActionTraceEventAttachment[]; attachments?: AfterActionTraceEventAttachment[];
annotations?: AfterActionTraceEventAnnotation[];
result?: any; result?: any;
point?: Point; point?: Point;
}; };

View File

@ -780,10 +780,35 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await showReport(); await showReport();
await page.click('text=example'); 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 () => {`); 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 }) => { test('should render annotations', async ({ runInlineTest, page, showReport }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'playwright.config.js': ` 'playwright.config.js': `

View File

@ -75,7 +75,9 @@ export default class MyReporter implements Reporter {
let location = ''; let location = '';
if (step.location) if (step.location)
location = formatLocation(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) { if (step.error) {
const errorLocation = this.printErrorLocation ? formatLocation(step.error.location) : ''; const errorLocation = this.printErrorLocation ? formatLocation(step.error.location) : '';
console.log(formatPrefix(step.category) + indent + '↪ error: ' + this.trimError(step.error.message!) + errorLocation); 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({ const result = await runInlineTest({
'a.test.ts': ` 'a.test.ts': `
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
test('steps with return values', async ({ page }) => { test('steps with return values', async ({ page }) => {
const v1 = await test.step('my step', (...args) => { const v1 = await test.step('my step', () => {
expect(args.length).toBe(0);
return 10; return 10;
}); });
console.log('v1 = ' + v1); console.log('v1 = ' + v1);
const v2 = await test.step('my step', async (...args) => { const v2 = await test.step('my step', async () => {
expect(args.length).toBe(0);
return new Promise(f => setTimeout(() => f(v1 + 10), 100)); return new Promise(f => setTimeout(() => f(v1 + 10), 100));
}); });
console.log('v2 = ' + v2); console.log('v2 = ' + v2);
@ -1549,9 +1549,9 @@ test('test.step.skip should work', async ({ runInlineTest }) => {
expect(result.report.stats.unexpected).toBe(0); expect(result.report.stats.unexpected).toBe(0);
expect(stripAnsi(result.output)).toBe(` expect(stripAnsi(result.output)).toBe(`
hook |Before Hooks 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 |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 test.step | inner step 2.2 @ a.test.ts:13
expect | expect.toBe @ a.test.ts:14 expect | expect.toBe @ a.test.ts:14
hook |After Hooks hook |After Hooks
@ -1581,12 +1581,56 @@ test('skip test.step.skip body', async ({ runInlineTest }) => {
expect(stripAnsi(result.output)).toBe(` expect(stripAnsi(result.output)).toBe(`
hook |Before Hooks hook |Before Hooks
test.step |outer step 2 @ a.test.ts:5 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 expect |expect.toBe @ a.test.ts:10
hook |After Hooks 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 }) => { test('show api calls inside expects', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'reporter.ts': stepIndentReporter, 'reporter.ts': stepIndentReporter,

View File

@ -163,8 +163,8 @@ export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
afterAll(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void; afterAll(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void; use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void;
step: { step: {
<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<T>; <T>(title: string, body: (step: TestStepInfo) => T | Promise<T>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<T>;
skip(title: string, body: () => any | Promise<any>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<void>; skip(title: string, body: (step: TestStepInfo) => any | Promise<any>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<void>;
} }
expect: Expect<{}>; expect: Expect<{}>;
extend<T extends {}, W extends {} = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>; extend<T extends {}, W extends {} = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>;