mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat: conditional step.skip() (#34578)
This commit is contained in:
parent
da12af24c2
commit
a1451c75f8
@ -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.
|
||||||
|
|
||||||
|
|||||||
39
docs/src/test-api/class-teststepinfo.md
Normal file
39
docs/src/test-api/class-teststepinfo.md
Normal 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.
|
||||||
@ -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]>>
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
37
packages/playwright/types/test.d.ts
vendored
37
packages/playwright/types/test.d.ts
vendored
@ -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
|
||||||
|
|||||||
15
packages/playwright/types/testReporter.d.ts
vendored
15
packages/playwright/types/testReporter.d.ts
vendored
@ -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).
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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': `
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
4
utils/generate_types/overrides-test.d.ts
vendored
4
utils/generate_types/overrides-test.d.ts
vendored
@ -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>;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user