parent
eacaf5fc89
commit
b1fdf0bcb6
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import type { RawStack } from './stackTrace';
|
import type { RawStack } from './stackTrace';
|
||||||
|
|
||||||
export type ZoneType = 'apiZone' | 'expectZone';
|
export type ZoneType = 'apiZone' | 'expectZone' | 'stepZone';
|
||||||
|
|
||||||
class ZoneManager {
|
class ZoneManager {
|
||||||
lastZoneId = 0;
|
lastZoneId = 0;
|
||||||
|
|
|
||||||
|
|
@ -82,10 +82,9 @@ export type TestEndPayload = {
|
||||||
export type StepBeginPayload = {
|
export type StepBeginPayload = {
|
||||||
testId: string;
|
testId: string;
|
||||||
stepId: string;
|
stepId: string;
|
||||||
|
parentStepId: string | undefined;
|
||||||
title: string;
|
title: string;
|
||||||
category: string;
|
category: string;
|
||||||
canHaveChildren: boolean;
|
|
||||||
forceNoParent: boolean;
|
|
||||||
wallTime: number; // milliseconds since unix epoch
|
wallTime: number; // milliseconds since unix epoch
|
||||||
location?: { file: string, line: number, column: number };
|
location?: { file: string, line: number, column: number };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import { currentlyLoadingFileSuite, currentTestInfo, setCurrentlyLoadingFileSuit
|
||||||
import { TestCase, Suite } from './test';
|
import { TestCase, Suite } from './test';
|
||||||
import { wrapFunctionWithLocation } from './transform';
|
import { wrapFunctionWithLocation } from './transform';
|
||||||
import type { Fixtures, FixturesWithLocation, Location, TestType } from './types';
|
import type { Fixtures, FixturesWithLocation, Location, TestType } from './types';
|
||||||
import { serializeError } from '../util';
|
|
||||||
|
|
||||||
const testTypeSymbol = Symbol('testType');
|
const testTypeSymbol = Symbol('testType');
|
||||||
|
|
||||||
|
|
@ -212,22 +211,11 @@ export class TestTypeImpl {
|
||||||
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`);
|
||||||
const step = testInfo._addStep({
|
return testInfo._runAsStep(body, {
|
||||||
category: 'test.step',
|
category: 'test.step',
|
||||||
title,
|
title,
|
||||||
location,
|
location,
|
||||||
canHaveChildren: true,
|
|
||||||
forceNoParent: false,
|
|
||||||
wallTime: Date.now(),
|
|
||||||
});
|
});
|
||||||
try {
|
|
||||||
const result = await body();
|
|
||||||
step.complete({});
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
step.complete({ error: serializeError(e) });
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _extend(location: Location, fixtures: Fixtures) {
|
private _extend(location: Location, fixtures: Fixtures) {
|
||||||
|
|
|
||||||
|
|
@ -272,8 +272,6 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
location: stackTrace?.frames[0] as any,
|
location: stackTrace?.frames[0] as any,
|
||||||
category: 'pw:api',
|
category: 'pw:api',
|
||||||
title: apiCall,
|
title: apiCall,
|
||||||
canHaveChildren: false,
|
|
||||||
forceNoParent: false,
|
|
||||||
wallTime,
|
wallTime,
|
||||||
});
|
});
|
||||||
userData.userObject = step;
|
userData.userObject = step;
|
||||||
|
|
|
||||||
|
|
@ -216,8 +216,6 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
location: stackFrames[0],
|
location: stackFrames[0],
|
||||||
category: 'expect',
|
category: 'expect',
|
||||||
title: trimLongString(customMessage || defaultTitle, 1024),
|
title: trimLongString(customMessage || defaultTitle, 1024),
|
||||||
canHaveChildren: true,
|
|
||||||
forceNoParent: false,
|
|
||||||
wallTime
|
wallTime
|
||||||
});
|
});
|
||||||
testInfo.currentStep = step;
|
testInfo.currentStep = step;
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ import type { FullConfigInternal } from '../common/types';
|
||||||
type TestResultData = {
|
type TestResultData = {
|
||||||
result: TestResult;
|
result: TestResult;
|
||||||
steps: Map<string, TestStep>;
|
steps: Map<string, TestStep>;
|
||||||
stepStack: Set<TestStep>;
|
|
||||||
};
|
};
|
||||||
type TestData = {
|
type TestData = {
|
||||||
test: TestCase;
|
test: TestCase;
|
||||||
|
|
@ -218,7 +217,7 @@ export class Dispatcher {
|
||||||
const onTestBegin = (params: TestBeginPayload) => {
|
const onTestBegin = (params: TestBeginPayload) => {
|
||||||
const data = this._testById.get(params.testId)!;
|
const data = this._testById.get(params.testId)!;
|
||||||
const result = data.test._appendTestResult();
|
const result = data.test._appendTestResult();
|
||||||
data.resultByWorkerIndex.set(worker.workerIndex, { result, stepStack: new Set(), steps: new Map() });
|
data.resultByWorkerIndex.set(worker.workerIndex, { result, steps: new Map() });
|
||||||
result.parallelIndex = worker.parallelIndex;
|
result.parallelIndex = worker.parallelIndex;
|
||||||
result.workerIndex = worker.workerIndex;
|
result.workerIndex = worker.workerIndex;
|
||||||
result.startTime = new Date(params.startWallTime);
|
result.startTime = new Date(params.startWallTime);
|
||||||
|
|
@ -267,8 +266,8 @@ export class Dispatcher {
|
||||||
// The test has finished, but steps are still coming. Just ignore them.
|
// The test has finished, but steps are still coming. Just ignore them.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { result, steps, stepStack } = runData;
|
const { result, steps } = runData;
|
||||||
const parentStep = params.forceNoParent ? undefined : [...stepStack].pop();
|
const parentStep = params.parentStepId ? steps.get(params.parentStepId) : undefined;
|
||||||
const step: TestStep = {
|
const step: TestStep = {
|
||||||
title: params.title,
|
title: params.title,
|
||||||
titlePath: () => {
|
titlePath: () => {
|
||||||
|
|
@ -284,8 +283,6 @@ export class Dispatcher {
|
||||||
};
|
};
|
||||||
steps.set(params.stepId, step);
|
steps.set(params.stepId, step);
|
||||||
(parentStep || result).steps.push(step);
|
(parentStep || result).steps.push(step);
|
||||||
if (params.canHaveChildren)
|
|
||||||
stepStack.add(step);
|
|
||||||
this._reporter.onStepBegin?.(data.test, result, step);
|
this._reporter.onStepBegin?.(data.test, result, step);
|
||||||
};
|
};
|
||||||
worker.on('stepBegin', onStepBegin);
|
worker.on('stepBegin', onStepBegin);
|
||||||
|
|
@ -297,7 +294,7 @@ export class Dispatcher {
|
||||||
// The test has finished, but steps are still coming. Just ignore them.
|
// The test has finished, but steps are still coming. Just ignore them.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { result, steps, stepStack } = runData;
|
const { result, steps } = runData;
|
||||||
const step = steps.get(params.stepId);
|
const step = steps.get(params.stepId);
|
||||||
if (!step) {
|
if (!step) {
|
||||||
this._reporter.onStdErr?.('Internal error: step end without step begin: ' + params.stepId, data.test, result);
|
this._reporter.onStdErr?.('Internal error: step end without step begin: ' + params.stepId, data.test, result);
|
||||||
|
|
@ -308,7 +305,6 @@ export class Dispatcher {
|
||||||
step.duration = params.wallTime - step.startTime.getTime();
|
step.duration = params.wallTime - step.startTime.getTime();
|
||||||
if (params.error)
|
if (params.error)
|
||||||
step.error = params.error;
|
step.error = params.error;
|
||||||
stepStack.delete(step);
|
|
||||||
steps.delete(params.stepId);
|
steps.delete(params.stepId);
|
||||||
this._reporter.onStepEnd?.(data.test, result, step);
|
this._reporter.onStepEnd?.(data.test, result, step);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -88,13 +88,23 @@ class Fixture {
|
||||||
const workerInfo: WorkerInfo = { config: testInfo.config, parallelIndex: testInfo.parallelIndex, workerIndex: testInfo.workerIndex, project: testInfo.project };
|
const workerInfo: WorkerInfo = { config: testInfo.config, parallelIndex: testInfo.parallelIndex, workerIndex: testInfo.workerIndex, project: testInfo.project };
|
||||||
const info = this.registration.scope === 'worker' ? workerInfo : testInfo;
|
const info = this.registration.scope === 'worker' ? workerInfo : testInfo;
|
||||||
testInfo._timeoutManager.setCurrentFixture(this._runnableDescription);
|
testInfo._timeoutManager.setCurrentFixture(this._runnableDescription);
|
||||||
this._selfTeardownComplete = Promise.resolve().then(() => this.registration.fn(params, useFunc, info)).catch((e: any) => {
|
|
||||||
|
const handleError = (e: any) => {
|
||||||
this.failed = true;
|
this.failed = true;
|
||||||
if (!useFuncStarted.isDone())
|
if (!useFuncStarted.isDone())
|
||||||
useFuncStarted.reject(e);
|
useFuncStarted.reject(e);
|
||||||
else
|
else
|
||||||
throw e;
|
throw e;
|
||||||
});
|
};
|
||||||
|
try {
|
||||||
|
const result = this.registration.fn(params, useFunc, info);
|
||||||
|
if (result instanceof Promise)
|
||||||
|
this._selfTeardownComplete = result.catch(handleError);
|
||||||
|
else
|
||||||
|
this._selfTeardownComplete = Promise.resolve();
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e);
|
||||||
|
}
|
||||||
await useFuncStarted;
|
await useFuncStarted;
|
||||||
testInfo._timeoutManager.setCurrentFixture(undefined);
|
testInfo._timeoutManager.setCurrentFixture(undefined);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { monotonicTime } from 'playwright-core/lib/utils';
|
import { captureRawStack, monotonicTime, zones } from 'playwright-core/lib/utils';
|
||||||
import type { TestInfoError, TestInfo, TestStatus } from '../../types/test';
|
import type { TestInfoError, TestInfo, TestStatus } from '../../types/test';
|
||||||
import type { StepBeginPayload, StepEndPayload, WorkerInitParams } from '../common/ipc';
|
import type { StepBeginPayload, StepEndPayload, WorkerInitParams } from '../common/ipc';
|
||||||
import type { TestCase } from '../common/test';
|
import type { TestCase } from '../common/test';
|
||||||
|
|
@ -33,10 +33,9 @@ export type TestInfoErrorState = {
|
||||||
|
|
||||||
interface TestStepInternal {
|
interface TestStepInternal {
|
||||||
complete(result: { error?: Error | TestInfoError }): void;
|
complete(result: { error?: Error | TestInfoError }): void;
|
||||||
|
stepId: string;
|
||||||
title: string;
|
title: string;
|
||||||
category: string;
|
category: string;
|
||||||
canHaveChildren: boolean;
|
|
||||||
forceNoParent: boolean;
|
|
||||||
wallTime: number;
|
wallTime: number;
|
||||||
location?: Location;
|
location?: Location;
|
||||||
refinedTitle?: string;
|
refinedTitle?: string;
|
||||||
|
|
@ -195,26 +194,34 @@ export class TestInfoImpl implements TestInfo {
|
||||||
this.duration = this._timeoutManager.defaultSlotTimings().elapsed | 0;
|
this.duration = this._timeoutManager.defaultSlotTimings().elapsed | 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _runFn(fn: Function, skips?: 'allowSkips'): Promise<TestInfoError | undefined> {
|
async _runFn(fn: () => Promise<void>, skips?: 'allowSkips', stepInfo?: Omit<TestStepInternal, 'complete' | 'wallTime' | 'parentStepId' | 'stepId'>): Promise<TestInfoError | undefined> {
|
||||||
|
const step = stepInfo ? this._addStep({ ...stepInfo, wallTime: Date.now() }) : undefined;
|
||||||
try {
|
try {
|
||||||
await fn();
|
if (step)
|
||||||
|
await zones.run('stepZone', step, fn);
|
||||||
|
else
|
||||||
|
await fn();
|
||||||
|
step?.complete({});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (skips === 'allowSkips' && error instanceof SkipError) {
|
if (skips === 'allowSkips' && error instanceof SkipError) {
|
||||||
if (this.status === 'passed')
|
if (this.status === 'passed')
|
||||||
this.status = 'skipped';
|
this.status = 'skipped';
|
||||||
} else {
|
} else {
|
||||||
const serialized = serializeError(error);
|
const serialized = serializeError(error);
|
||||||
|
step?.complete({ error: serialized });
|
||||||
this._failWithError(serialized, true /* isHardError */);
|
this._failWithError(serialized, true /* isHardError */);
|
||||||
return serialized;
|
return serialized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_addStep(data: Omit<TestStepInternal, 'complete'>): TestStepInternal {
|
_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId'>): TestStepInternal {
|
||||||
const stepId = `${data.category}@${data.title}@${++this._lastStepId}`;
|
const stepId = `${data.category}@${data.title}@${++this._lastStepId}`;
|
||||||
|
const parentStep = zones.zoneData<TestStepInternal>('stepZone', captureRawStack());
|
||||||
let callbackHandled = false;
|
let callbackHandled = false;
|
||||||
const firstErrorIndex = this.errors.length;
|
const firstErrorIndex = this.errors.length;
|
||||||
const step: TestStepInternal = {
|
const step: TestStepInternal = {
|
||||||
|
stepId,
|
||||||
...data,
|
...data,
|
||||||
complete: result => {
|
complete: result => {
|
||||||
if (callbackHandled)
|
if (callbackHandled)
|
||||||
|
|
@ -248,6 +255,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
const payload: StepBeginPayload = {
|
const payload: StepBeginPayload = {
|
||||||
testId: this._test.id,
|
testId: this._test.id,
|
||||||
stepId,
|
stepId,
|
||||||
|
parentStepId: parentStep ? parentStep.stepId : undefined,
|
||||||
...data,
|
...data,
|
||||||
location,
|
location,
|
||||||
};
|
};
|
||||||
|
|
@ -292,16 +300,18 @@ export class TestInfoImpl implements TestInfo {
|
||||||
this._hasHardError = state.hasHardError;
|
this._hasHardError = state.hasHardError;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _runAsStep<T>(cb: () => Promise<T>, stepInfo: Omit<TestStepInternal, 'complete' | 'wallTime'>): Promise<T> {
|
async _runAsStep<T>(cb: (step: TestStepInternal) => Promise<T>, stepInfo: Omit<TestStepInternal, 'complete' | 'wallTime' | 'parentStepId' | 'stepId'>): Promise<T> {
|
||||||
const step = this._addStep({ ...stepInfo, wallTime: Date.now() });
|
const step = this._addStep({ ...stepInfo, wallTime: Date.now() });
|
||||||
try {
|
return await zones.run('stepZone', step, async () => {
|
||||||
const result = await cb();
|
try {
|
||||||
step.complete({});
|
const result = await cb(step);
|
||||||
return result;
|
step.complete({});
|
||||||
} catch (e) {
|
return result;
|
||||||
step.complete({ error: e instanceof SkipError ? undefined : serializeError(e) });
|
} catch (e) {
|
||||||
throw e;
|
step.complete({ error: e instanceof SkipError ? undefined : serializeError(e) });
|
||||||
}
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_isFailure() {
|
_isFailure() {
|
||||||
|
|
|
||||||
|
|
@ -320,17 +320,10 @@ export class WorkerMain extends ProcessRunner {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const beforeHooksStep = testInfo._addStep({
|
let params: object | null = null;
|
||||||
category: 'hook',
|
|
||||||
title: 'Before Hooks',
|
|
||||||
canHaveChildren: true,
|
|
||||||
forceNoParent: true,
|
|
||||||
wallTime: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Note: wrap all preparation steps together, because failure/skip in any of them
|
// Note: wrap all preparation steps together, because failure/skip in any of them
|
||||||
// prevents further setup and/or test from running.
|
// prevents further setup and/or test from running.
|
||||||
const maybeError = await testInfo._runFn(async () => {
|
await testInfo._runFn(async () => {
|
||||||
// Run "beforeAll" modifiers on parent suites, unless already run during previous tests.
|
// Run "beforeAll" modifiers on parent suites, unless already run during previous tests.
|
||||||
for (const suite of suites) {
|
for (const suite of suites) {
|
||||||
if (this._extraSuiteAnnotations.has(suite))
|
if (this._extraSuiteAnnotations.has(suite))
|
||||||
|
|
@ -362,21 +355,24 @@ export class WorkerMain extends ProcessRunner {
|
||||||
|
|
||||||
// Setup fixtures required by the test.
|
// Setup fixtures required by the test.
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type: 'test' });
|
testInfo._timeoutManager.setCurrentRunnable({ type: 'test' });
|
||||||
const params = await this._fixtureRunner.resolveParametersForFunction(test.fn, testInfo, 'test');
|
params = await this._fixtureRunner.resolveParametersForFunction(test.fn, testInfo, 'test');
|
||||||
beforeHooksStep.complete({}); // Report fixture hooks step as completed.
|
}, 'allowSkips', {
|
||||||
if (params === null) {
|
category: 'hook',
|
||||||
// Fixture setup failed, we should not run the test now.
|
title: 'Before Hooks',
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
|
if (params === null) {
|
||||||
|
// Fixture setup failed, we should not run the test now.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await testInfo._runFn(async () => {
|
||||||
// Now run the test itself.
|
// Now run the test itself.
|
||||||
debugTest(`test function started`);
|
debugTest(`test function started`);
|
||||||
const fn = test.fn; // Extract a variable to get a better stack trace ("myTest" vs "TestCase.myTest [as fn]").
|
const fn = test.fn; // Extract a variable to get a better stack trace ("myTest" vs "TestCase.myTest [as fn]").
|
||||||
await fn(params, testInfo);
|
await fn(params, testInfo);
|
||||||
debugTest(`test function finished`);
|
debugTest(`test function finished`);
|
||||||
}, 'allowSkips');
|
}, 'allowSkips');
|
||||||
|
|
||||||
beforeHooksStep.complete({ error: maybeError }); // Second complete is a no-op.
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (didFailBeforeAllForSuite) {
|
if (didFailBeforeAllForSuite) {
|
||||||
|
|
@ -386,100 +382,96 @@ export class WorkerMain extends ProcessRunner {
|
||||||
this._skipRemainingTestsInSuite = didFailBeforeAllForSuite;
|
this._skipRemainingTestsInSuite = didFailBeforeAllForSuite;
|
||||||
}
|
}
|
||||||
|
|
||||||
const afterHooksStep = testInfo._addStep({
|
|
||||||
category: 'hook',
|
|
||||||
title: 'After Hooks',
|
|
||||||
canHaveChildren: true,
|
|
||||||
forceNoParent: true,
|
|
||||||
wallTime: Date.now(),
|
|
||||||
});
|
|
||||||
let firstAfterHooksError: TestInfoError | undefined;
|
|
||||||
|
|
||||||
let afterHooksSlot: TimeSlot | undefined;
|
let afterHooksSlot: TimeSlot | undefined;
|
||||||
if (testInfo._didTimeout) {
|
if (testInfo._didTimeout) {
|
||||||
// A timed-out test gets a full additional timeout to run after hooks.
|
// A timed-out test gets a full additional timeout to run after hooks.
|
||||||
afterHooksSlot = { timeout: this._project.timeout, elapsed: 0 };
|
afterHooksSlot = { timeout: this._project.timeout, elapsed: 0 };
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type: 'afterEach', slot: afterHooksSlot });
|
testInfo._timeoutManager.setCurrentRunnable({ type: 'afterEach', slot: afterHooksSlot });
|
||||||
}
|
}
|
||||||
await testInfo._runWithTimeout(async () => {
|
await testInfo._runAsStep(async step => {
|
||||||
// Note: do not wrap all teardown steps together, because failure in any of them
|
let firstAfterHooksError: TestInfoError | undefined;
|
||||||
// does not prevent further teardown steps from running.
|
|
||||||
|
|
||||||
// Run "immediately upon test failure" callbacks.
|
|
||||||
if (testInfo._isFailure()) {
|
|
||||||
const onFailureError = await testInfo._runFn(async () => {
|
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type: 'test', slot: afterHooksSlot });
|
|
||||||
for (const [fn, title] of testInfo._onTestFailureImmediateCallbacks) {
|
|
||||||
debugTest(`on-failure callback started`);
|
|
||||||
await testInfo._runAsStep(fn, {
|
|
||||||
category: 'hook',
|
|
||||||
title,
|
|
||||||
canHaveChildren: true,
|
|
||||||
forceNoParent: false,
|
|
||||||
});
|
|
||||||
debugTest(`on-failure callback finished`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
firstAfterHooksError = firstAfterHooksError || onFailureError;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run "afterEach" hooks, unless we failed at beforeAll stage.
|
|
||||||
if (shouldRunAfterEachHooks) {
|
|
||||||
const afterEachError = await testInfo._runFn(() => this._runEachHooksForSuites(reversedSuites, 'afterEach', testInfo, afterHooksSlot));
|
|
||||||
firstAfterHooksError = firstAfterHooksError || afterEachError;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run "afterAll" hooks for suites that are not shared with the next test.
|
|
||||||
// In case of failure the worker will be stopped and we have to make sure that afterAll
|
|
||||||
// hooks run before test fixtures teardown.
|
|
||||||
for (const suite of reversedSuites) {
|
|
||||||
if (!nextSuites.has(suite) || testInfo._isFailure()) {
|
|
||||||
const afterAllError = await this._runAfterAllHooksForSuite(suite, testInfo);
|
|
||||||
firstAfterHooksError = firstAfterHooksError || afterAllError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Teardown test-scoped fixtures. Attribute to 'test' so that users understand
|
|
||||||
// they should probably increate the test timeout to fix this issue.
|
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type: 'test', slot: afterHooksSlot });
|
|
||||||
debugTest(`tearing down test scope started`);
|
|
||||||
const testScopeError = await testInfo._runFn(() => this._fixtureRunner.teardownScope('test', testInfo._timeoutManager));
|
|
||||||
debugTest(`tearing down test scope finished`);
|
|
||||||
firstAfterHooksError = firstAfterHooksError || testScopeError;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (testInfo._isFailure())
|
|
||||||
this._isStopped = true;
|
|
||||||
|
|
||||||
if (this._isStopped) {
|
|
||||||
// Run all remaining "afterAll" hooks and teardown all fixtures when worker is shutting down.
|
|
||||||
// Mark as "cleaned up" early to avoid running cleanup twice.
|
|
||||||
this._didRunFullCleanup = true;
|
|
||||||
|
|
||||||
// Give it more time for the full cleanup.
|
|
||||||
await testInfo._runWithTimeout(async () => {
|
await testInfo._runWithTimeout(async () => {
|
||||||
debugTest(`running full cleanup after the failure`);
|
// Note: do not wrap all teardown steps together, because failure in any of them
|
||||||
for (const suite of reversedSuites) {
|
// does not prevent further teardown steps from running.
|
||||||
const afterAllError = await this._runAfterAllHooksForSuite(suite, testInfo);
|
|
||||||
firstAfterHooksError = firstAfterHooksError || afterAllError;
|
// Run "immediately upon test failure" callbacks.
|
||||||
|
if (testInfo._isFailure()) {
|
||||||
|
const onFailureError = await testInfo._runFn(async () => {
|
||||||
|
testInfo._timeoutManager.setCurrentRunnable({ type: 'test', slot: afterHooksSlot });
|
||||||
|
for (const [fn, title] of testInfo._onTestFailureImmediateCallbacks) {
|
||||||
|
debugTest(`on-failure callback started`);
|
||||||
|
await testInfo._runAsStep(fn, {
|
||||||
|
category: 'hook',
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
debugTest(`on-failure callback finished`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
firstAfterHooksError = firstAfterHooksError || onFailureError;
|
||||||
}
|
}
|
||||||
const teardownSlot = { timeout: this._project.timeout, elapsed: 0 };
|
|
||||||
// Attribute to 'test' so that users understand they should probably increate the test timeout to fix this issue.
|
// Run "afterEach" hooks, unless we failed at beforeAll stage.
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type: 'test', slot: teardownSlot });
|
if (shouldRunAfterEachHooks) {
|
||||||
|
const afterEachError = await testInfo._runFn(() => this._runEachHooksForSuites(reversedSuites, 'afterEach', testInfo, afterHooksSlot));
|
||||||
|
firstAfterHooksError = firstAfterHooksError || afterEachError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run "afterAll" hooks for suites that are not shared with the next test.
|
||||||
|
// In case of failure the worker will be stopped and we have to make sure that afterAll
|
||||||
|
// hooks run before test fixtures teardown.
|
||||||
|
for (const suite of reversedSuites) {
|
||||||
|
if (!nextSuites.has(suite) || testInfo._isFailure()) {
|
||||||
|
const afterAllError = await this._runAfterAllHooksForSuite(suite, testInfo);
|
||||||
|
firstAfterHooksError = firstAfterHooksError || afterAllError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teardown test-scoped fixtures. Attribute to 'test' so that users understand
|
||||||
|
// they should probably increate the test timeout to fix this issue.
|
||||||
|
testInfo._timeoutManager.setCurrentRunnable({ type: 'test', slot: afterHooksSlot });
|
||||||
debugTest(`tearing down test scope started`);
|
debugTest(`tearing down test scope started`);
|
||||||
const testScopeError = await testInfo._runFn(() => this._fixtureRunner.teardownScope('test', testInfo._timeoutManager));
|
const testScopeError = await testInfo._runFn(() => this._fixtureRunner.teardownScope('test', testInfo._timeoutManager));
|
||||||
debugTest(`tearing down test scope finished`);
|
debugTest(`tearing down test scope finished`);
|
||||||
firstAfterHooksError = firstAfterHooksError || testScopeError;
|
firstAfterHooksError = firstAfterHooksError || testScopeError;
|
||||||
// Attribute to 'teardown' because worker fixtures are not perceived as a part of a test.
|
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type: 'teardown', slot: teardownSlot });
|
|
||||||
debugTest(`tearing down worker scope started`);
|
|
||||||
const workerScopeError = await testInfo._runFn(() => this._fixtureRunner.teardownScope('worker', testInfo._timeoutManager));
|
|
||||||
debugTest(`tearing down worker scope finished`);
|
|
||||||
firstAfterHooksError = firstAfterHooksError || workerScopeError;
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
afterHooksStep.complete({ error: firstAfterHooksError });
|
if (testInfo._isFailure())
|
||||||
|
this._isStopped = true;
|
||||||
|
|
||||||
|
if (this._isStopped) {
|
||||||
|
// Run all remaining "afterAll" hooks and teardown all fixtures when worker is shutting down.
|
||||||
|
// Mark as "cleaned up" early to avoid running cleanup twice.
|
||||||
|
this._didRunFullCleanup = true;
|
||||||
|
|
||||||
|
// Give it more time for the full cleanup.
|
||||||
|
await testInfo._runWithTimeout(async () => {
|
||||||
|
debugTest(`running full cleanup after the failure`);
|
||||||
|
for (const suite of reversedSuites) {
|
||||||
|
const afterAllError = await this._runAfterAllHooksForSuite(suite, testInfo);
|
||||||
|
firstAfterHooksError = firstAfterHooksError || afterAllError;
|
||||||
|
}
|
||||||
|
const teardownSlot = { timeout: this._project.timeout, elapsed: 0 };
|
||||||
|
// Attribute to 'test' so that users understand they should probably increate the test timeout to fix this issue.
|
||||||
|
testInfo._timeoutManager.setCurrentRunnable({ type: 'test', slot: teardownSlot });
|
||||||
|
debugTest(`tearing down test scope started`);
|
||||||
|
const testScopeError = await testInfo._runFn(() => this._fixtureRunner.teardownScope('test', testInfo._timeoutManager));
|
||||||
|
debugTest(`tearing down test scope finished`);
|
||||||
|
firstAfterHooksError = firstAfterHooksError || testScopeError;
|
||||||
|
// Attribute to 'teardown' because worker fixtures are not perceived as a part of a test.
|
||||||
|
testInfo._timeoutManager.setCurrentRunnable({ type: 'teardown', slot: teardownSlot });
|
||||||
|
debugTest(`tearing down worker scope started`);
|
||||||
|
const workerScopeError = await testInfo._runFn(() => this._fixtureRunner.teardownScope('worker', testInfo._timeoutManager));
|
||||||
|
debugTest(`tearing down worker scope finished`);
|
||||||
|
firstAfterHooksError = firstAfterHooksError || workerScopeError;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (firstAfterHooksError)
|
||||||
|
step.complete({ error: firstAfterHooksError });
|
||||||
|
}, {
|
||||||
|
category: 'hook',
|
||||||
|
title: 'After Hooks',
|
||||||
|
});
|
||||||
|
|
||||||
this._currentTest = null;
|
this._currentTest = null;
|
||||||
setCurrentTestInfo(null);
|
setCurrentTestInfo(null);
|
||||||
this.dispatchEvent('testEnd', buildTestEndPayload(testInfo));
|
this.dispatchEvent('testEnd', buildTestEndPayload(testInfo));
|
||||||
|
|
@ -500,8 +492,6 @@ export class WorkerMain extends ProcessRunner {
|
||||||
const result = await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(modifier.fn, testInfo, scope), {
|
const result = await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(modifier.fn, testInfo, scope), {
|
||||||
category: 'hook',
|
category: 'hook',
|
||||||
title: `${modifier.type} modifier`,
|
title: `${modifier.type} modifier`,
|
||||||
canHaveChildren: true,
|
|
||||||
forceNoParent: false,
|
|
||||||
location: modifier.location,
|
location: modifier.location,
|
||||||
});
|
});
|
||||||
debugTest(`modifier at "${formatLocation(modifier.location)}" finished`);
|
debugTest(`modifier at "${formatLocation(modifier.location)}" finished`);
|
||||||
|
|
@ -527,8 +517,6 @@ export class WorkerMain extends ProcessRunner {
|
||||||
await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only'), {
|
await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only'), {
|
||||||
category: 'hook',
|
category: 'hook',
|
||||||
title: `${hook.type} hook`,
|
title: `${hook.type} hook`,
|
||||||
canHaveChildren: true,
|
|
||||||
forceNoParent: false,
|
|
||||||
location: hook.location,
|
location: hook.location,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -557,8 +545,6 @@ export class WorkerMain extends ProcessRunner {
|
||||||
await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only'), {
|
await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only'), {
|
||||||
category: 'hook',
|
category: 'hook',
|
||||||
title: `${hook.type} hook`,
|
title: `${hook.type} hook`,
|
||||||
canHaveChildren: true,
|
|
||||||
forceNoParent: false,
|
|
||||||
location: hook.location,
|
location: hook.location,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -577,8 +563,6 @@ export class WorkerMain extends ProcessRunner {
|
||||||
await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'test'), {
|
await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'test'), {
|
||||||
category: 'hook',
|
category: 'hook',
|
||||||
title: `${hook.type} hook`,
|
title: `${hook.type} hook`,
|
||||||
canHaveChildren: true,
|
|
||||||
forceNoParent: false,
|
|
||||||
location: hook.location,
|
location: hook.location,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -599,3 +599,23 @@ test('should report skipped tests in-order with correct properties', async ({ ru
|
||||||
'retries-3',
|
'retries-3',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should skip tests if beforeEach has skip', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test.beforeEach(() => {
|
||||||
|
test.skip();
|
||||||
|
});
|
||||||
|
test('no marker', () => {
|
||||||
|
console.log('skip-me');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
const expectTest = expectTestHelper(result);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(0);
|
||||||
|
expect(result.skipped).toBe(1);
|
||||||
|
expectTest('no marker', 'skipped', 'skipped', ['skip']);
|
||||||
|
expect(result.output).not.toContain('skip-me');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -457,3 +457,175 @@ test('should mark step as failed when soft expect fails', async ({ runInlineTest
|
||||||
{ title: 'After Hooks', category: 'hook' }
|
{ title: 'After Hooks', category: 'hook' }
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should nest steps based on zones', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'reporter.ts': stepHierarchyReporter,
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
reporter: './reporter',
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
await test.step('in beforeAll', () => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await test.step('in afterAll', () => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.beforeEach(async () => {
|
||||||
|
await test.step('in beforeEach', () => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async () => {
|
||||||
|
await test.step('in afterEach', () => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.only('foo', async ({ page }) => {
|
||||||
|
await test.step('grand', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
test.step('parent1', async () => {
|
||||||
|
await test.step('child1', async () => {
|
||||||
|
await page.click('body');
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
test.step('parent2', async () => {
|
||||||
|
await test.step('child2', async () => {
|
||||||
|
await expect(page.locator('body')).toBeVisible();
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { reporter: '', workers: 1 });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
const objects = result.outputLines.map(line => JSON.parse(line));
|
||||||
|
expect(objects).toEqual([
|
||||||
|
{
|
||||||
|
title: 'Before Hooks',
|
||||||
|
category: 'hook',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: 'beforeAll hook',
|
||||||
|
category: 'hook',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: 'in beforeAll',
|
||||||
|
category: 'test.step',
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'beforeEach hook',
|
||||||
|
category: 'hook',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: 'in beforeEach',
|
||||||
|
category: 'test.step',
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'browserContext.newPage',
|
||||||
|
category: 'pw:api'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'grand',
|
||||||
|
category: 'test.step',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: 'parent1',
|
||||||
|
category: 'test.step',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: 'child1',
|
||||||
|
category: 'test.step',
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' },
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: 'page.click(body)',
|
||||||
|
category: 'pw:api',
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
location: {
|
||||||
|
file: 'a.test.ts',
|
||||||
|
line: 'number',
|
||||||
|
column: 'number'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'parent2',
|
||||||
|
category: 'test.step',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: 'child2',
|
||||||
|
category: 'test.step',
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' },
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: 'expect.toBeVisible',
|
||||||
|
category: 'expect',
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
location: {
|
||||||
|
file: 'a.test.ts',
|
||||||
|
line: 'number',
|
||||||
|
column: 'number'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'After Hooks',
|
||||||
|
category: 'hook',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: 'afterEach hook',
|
||||||
|
category: 'hook',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: 'in afterEach',
|
||||||
|
category: 'test.step',
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'afterAll hook',
|
||||||
|
category: 'hook',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: 'in afterAll',
|
||||||
|
category: 'test.step',
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'browserContext.close',
|
||||||
|
category: 'pw:api'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue