feat(test runner): support test fixtures in beforeAll/afterAll (#8082)
Each hook gets its own test scope. This is not too useful for object fixtures like `page` (although one can use a page in `beforeAll` to save storage state), but much more useful for option fixtures like `viewport`.
This commit is contained in:
parent
41949e559e
commit
87548f94c1
|
|
@ -64,9 +64,9 @@ Test function that takes one or two arguments: an object with fixtures and optio
|
|||
Declares an `afterAll` hook that is executed once after all tests. When called in the scope of a test file, runs after all tests in the file. When called inside a [`method: Test.describe`] group, runs after all tests in the group.
|
||||
|
||||
### param: Test.afterAll.hookFunction
|
||||
- `hookFunction` <[function]\([Fixtures], [WorkerInfo]\)>
|
||||
- `hookFunction` <[function]\([Fixtures], [TestInfo]\)>
|
||||
|
||||
Hook function that takes one or two arguments: an object with fixtures and optional [WorkerInfo].
|
||||
Hook function that takes one or two arguments: an object with fixtures and optional [TestInfo].
|
||||
|
||||
|
||||
|
||||
|
|
@ -121,9 +121,9 @@ test('my test', async ({ page }) => {
|
|||
You can use [`method: Test.afterAll`] to teardown any resources set up in `beforeAll`.
|
||||
|
||||
### param: Test.beforeAll.hookFunction
|
||||
- `hookFunction` <[function]\([Fixtures], [WorkerInfo]\)>
|
||||
- `hookFunction` <[function]\([Fixtures], [TestInfo]\)>
|
||||
|
||||
Hook function that takes one or two arguments: an object with fixtures and optional [WorkerInfo].
|
||||
Hook function that takes one or two arguments: an object with fixtures and optional [TestInfo].
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class Fixture {
|
|||
this.value = null;
|
||||
}
|
||||
|
||||
async setup(info: any) {
|
||||
async setup(workerInfo: WorkerInfo, testInfo: TestInfo | undefined) {
|
||||
if (typeof this.registration.fn !== 'function') {
|
||||
this._setup = true;
|
||||
this.value = this.registration.fn;
|
||||
|
|
@ -57,7 +57,7 @@ class Fixture {
|
|||
const params: { [key: string]: any } = {};
|
||||
for (const name of this.registration.deps) {
|
||||
const registration = this.runner.pool!.resolveDependency(this.registration, name)!;
|
||||
const dep = await this.runner.setupFixtureForRegistration(registration, info);
|
||||
const dep = await this.runner.setupFixtureForRegistration(registration, workerInfo, testInfo);
|
||||
dep.usages.add(this);
|
||||
params[name] = dep.value;
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ class Fixture {
|
|||
this.value = value;
|
||||
setupFenceFulfill();
|
||||
return await teardownFence;
|
||||
}, info)).catch((e: any) => {
|
||||
}, this.registration.scope === 'worker' ? workerInfo : testInfo)).catch((e: any) => {
|
||||
if (!this._setup)
|
||||
setupFenceReject(e);
|
||||
else
|
||||
|
|
@ -187,7 +187,7 @@ export class FixturePool {
|
|||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
validateFunction(fn: Function, prefix: string, allowTestFixtures: boolean, location: Location) {
|
||||
validateFunction(fn: Function, prefix: string, location: Location) {
|
||||
const visit = (registration: FixtureRegistration) => {
|
||||
for (const name of registration.deps)
|
||||
visit(this.resolveDependency(registration, name)!);
|
||||
|
|
@ -196,8 +196,6 @@ export class FixturePool {
|
|||
const registration = this.registrations.get(name);
|
||||
if (!registration)
|
||||
throw errorWithLocations(`${prefix} has unknown parameter "${name}".`, { location, name: prefix, quoted: false });
|
||||
if (!allowTestFixtures && registration.scope === 'test')
|
||||
throw errorWithLocations(`${prefix} cannot depend on a test fixture "${name}".`, { location, name: prefix, quoted: false }, registration);
|
||||
visit(registration);
|
||||
}
|
||||
}
|
||||
|
|
@ -222,8 +220,10 @@ export class FixtureRunner {
|
|||
this.pool = pool;
|
||||
}
|
||||
|
||||
async teardownScope(scope: string) {
|
||||
for (const [, fixture] of this.instanceForId) {
|
||||
async teardownScope(scope: FixtureScope) {
|
||||
// Teardown fixtures in the reverse order.
|
||||
const fixtures = Array.from(this.instanceForId.values()).reverse();
|
||||
for (const fixture of fixtures) {
|
||||
if (fixture.registration.scope === scope)
|
||||
await fixture.teardown();
|
||||
}
|
||||
|
|
@ -231,12 +231,12 @@ export class FixtureRunner {
|
|||
this.testScopeClean = true;
|
||||
}
|
||||
|
||||
async resolveParametersAndRunHookOrTest(fn: Function, scope: FixtureScope, info: WorkerInfo|TestInfo) {
|
||||
async resolveParametersAndRunHookOrTest(fn: Function, workerInfo: WorkerInfo, testInfo: TestInfo | undefined) {
|
||||
// Install all automatic fixtures.
|
||||
for (const registration of this.pool!.registrations.values()) {
|
||||
const shouldSkip = scope === 'worker' && registration.scope === 'test';
|
||||
const shouldSkip = !testInfo && registration.scope === 'test';
|
||||
if (registration.auto && !shouldSkip)
|
||||
await this.setupFixtureForRegistration(registration, info);
|
||||
await this.setupFixtureForRegistration(registration, workerInfo, testInfo);
|
||||
}
|
||||
|
||||
// Install used fixtures.
|
||||
|
|
@ -244,14 +244,14 @@ export class FixtureRunner {
|
|||
const params: { [key: string]: any } = {};
|
||||
for (const name of names) {
|
||||
const registration = this.pool!.registrations.get(name)!;
|
||||
const fixture = await this.setupFixtureForRegistration(registration, info);
|
||||
const fixture = await this.setupFixtureForRegistration(registration, workerInfo, testInfo);
|
||||
params[name] = fixture.value;
|
||||
}
|
||||
|
||||
return fn(params, info);
|
||||
return fn(params, testInfo || workerInfo);
|
||||
}
|
||||
|
||||
async setupFixtureForRegistration(registration: FixtureRegistration, info: any): Promise<Fixture> {
|
||||
async setupFixtureForRegistration(registration: FixtureRegistration, workerInfo: WorkerInfo, testInfo: TestInfo | undefined): Promise<Fixture> {
|
||||
if (registration.scope === 'test')
|
||||
this.testScopeClean = false;
|
||||
|
||||
|
|
@ -261,7 +261,7 @@ export class FixtureRunner {
|
|||
|
||||
fixture = new Fixture(this, registration);
|
||||
this.instanceForId.set(registration.id, fixture);
|
||||
await fixture.setup(info);
|
||||
await fixture.setup(workerInfo, testInfo);
|
||||
return fixture;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,18 +67,27 @@ export class ProjectImpl {
|
|||
}
|
||||
this.testPools.set(test, pool);
|
||||
|
||||
pool.validateFunction(test.fn, 'Test', true, test.location);
|
||||
pool.validateFunction(test.fn, 'Test', test.location);
|
||||
for (let parent = test.parent; parent; parent = parent.parent) {
|
||||
for (const hook of parent._hooks)
|
||||
pool.validateFunction(hook.fn, hook.type + ' hook', hook.type === 'beforeEach' || hook.type === 'afterEach', hook.location);
|
||||
for (const hook of parent._eachHooks)
|
||||
pool.validateFunction(hook.fn, hook.type + ' hook', hook.location);
|
||||
for (const hook of parent._allHooks)
|
||||
pool.validateFunction(hook.fn, hook._type + ' hook', hook.location);
|
||||
for (const modifier of parent._modifiers)
|
||||
pool.validateFunction(modifier.fn, modifier.type + ' modifier', true, modifier.location);
|
||||
pool.validateFunction(modifier.fn, modifier.type + ' modifier', modifier.location);
|
||||
}
|
||||
}
|
||||
return this.testPools.get(test)!;
|
||||
}
|
||||
|
||||
private _cloneEntries(from: Suite, to: Suite, repeatEachIndex: number, filter: (test: TestCase) => boolean): boolean {
|
||||
for (const hook of from._allHooks) {
|
||||
const clone = hook._clone();
|
||||
clone.projectName = this.config.name;
|
||||
clone._pool = this.buildPool(hook);
|
||||
clone._projectIndex = this.index;
|
||||
to._addAllHook(clone);
|
||||
}
|
||||
for (const entry of from._entries) {
|
||||
if (entry instanceof Suite) {
|
||||
const suite = entry._clone();
|
||||
|
|
|
|||
|
|
@ -50,11 +50,8 @@ export class Suite extends Base implements reporterTypes.Suite {
|
|||
location?: Location;
|
||||
_fixtureOverrides: any = {};
|
||||
_entries: (Suite | TestCase)[] = [];
|
||||
_hooks: {
|
||||
type: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll',
|
||||
fn: Function,
|
||||
location: Location,
|
||||
}[] = [];
|
||||
_allHooks: TestCase[] = [];
|
||||
_eachHooks: { type: 'beforeEach' | 'afterEach', fn: Function, location: Location }[] = [];
|
||||
_timeout: number | undefined;
|
||||
_annotations: Annotations = [];
|
||||
_modifiers: Modifier[] = [];
|
||||
|
|
@ -71,6 +68,11 @@ export class Suite extends Base implements reporterTypes.Suite {
|
|||
this._entries.push(suite);
|
||||
}
|
||||
|
||||
_addAllHook(hook: TestCase) {
|
||||
hook.parent = this;
|
||||
this._allHooks.push(hook);
|
||||
}
|
||||
|
||||
allTests(): TestCase[] {
|
||||
const result: TestCase[] = [];
|
||||
const visit = (suite: Suite) => {
|
||||
|
|
@ -105,7 +107,7 @@ export class Suite extends Base implements reporterTypes.Suite {
|
|||
suite.location = this.location;
|
||||
suite._requireFile = this._requireFile;
|
||||
suite._fixtureOverrides = this._fixtureOverrides;
|
||||
suite._hooks = this._hooks.slice();
|
||||
suite._eachHooks = this._eachHooks.slice();
|
||||
suite._timeout = this._timeout;
|
||||
suite._annotations = this._annotations.slice();
|
||||
suite._modifiers = this._modifiers.slice();
|
||||
|
|
@ -124,6 +126,7 @@ export class TestCase extends Base implements reporterTypes.TestCase {
|
|||
projectName = '';
|
||||
retries = 0;
|
||||
|
||||
_type: 'beforeAll' | 'afterAll' | 'test';
|
||||
_ordinalInFile: number;
|
||||
_testType: TestTypeImpl;
|
||||
_id = '';
|
||||
|
|
@ -132,8 +135,9 @@ export class TestCase extends Base implements reporterTypes.TestCase {
|
|||
_repeatEachIndex = 0;
|
||||
_projectIndex = 0;
|
||||
|
||||
constructor(title: string, fn: Function, ordinalInFile: number, testType: TestTypeImpl, location: Location) {
|
||||
constructor(type: 'beforeAll' | 'afterAll' | 'test', title: string, fn: Function, ordinalInFile: number, testType: TestTypeImpl, location: Location) {
|
||||
super(title);
|
||||
this._type = type;
|
||||
this.fn = fn;
|
||||
this._ordinalInFile = ordinalInFile;
|
||||
this._testType = testType;
|
||||
|
|
@ -156,7 +160,7 @@ export class TestCase extends Base implements reporterTypes.TestCase {
|
|||
}
|
||||
|
||||
_clone(): TestCase {
|
||||
const test = new TestCase(this.title, this.fn, this._ordinalInFile, this._testType, this.location);
|
||||
const test = new TestCase(this._type, this.title, this.fn, this._ordinalInFile, this._testType, this.location);
|
||||
test._only = this._only;
|
||||
test._requireFile = this._requireFile;
|
||||
test.expectedStatus = this.expectedStatus;
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export class TestTypeImpl {
|
|||
const ordinalInFile = countByFile.get(suite._requireFile) || 0;
|
||||
countByFile.set(suite._requireFile, ordinalInFile + 1);
|
||||
|
||||
const test = new TestCase(title, fn, ordinalInFile, this, location);
|
||||
const test = new TestCase('test', title, fn, ordinalInFile, this, location);
|
||||
test._requireFile = suite._requireFile;
|
||||
suite._addTest(test);
|
||||
|
||||
|
|
@ -107,7 +107,13 @@ export class TestTypeImpl {
|
|||
const suite = currentlyLoadingFileSuite();
|
||||
if (!suite)
|
||||
throw errorWithLocation(location, `${name} hook can only be called in a test file`);
|
||||
suite._hooks.push({ type: name, fn, location });
|
||||
if (name === 'beforeAll' || name === 'afterAll') {
|
||||
const hook = new TestCase(name, name, fn, 0, this, location);
|
||||
hook._requireFile = suite._requireFile;
|
||||
suite._addAllHook(hook);
|
||||
} else {
|
||||
suite._eachHooks.push({ type: name, fn, location });
|
||||
}
|
||||
}
|
||||
|
||||
private _modifier(type: 'skip' | 'fail' | 'fixme' | 'slow', location: Location, ...modifierArgs: [arg?: any | Function, description?: string]) {
|
||||
|
|
|
|||
|
|
@ -29,5 +29,6 @@ export type CompleteStepCallback = (error?: Error | TestError) => void;
|
|||
|
||||
export interface TestInfoImpl extends TestInfo {
|
||||
_testFinished: Promise<void>;
|
||||
_type: 'test' | 'beforeAll' | 'afterAll';
|
||||
_addStep: (category: string, title: string) => CompleteStepCallback;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export class WorkerRunner extends EventEmitter {
|
|||
unhandledError(error: Error | any) {
|
||||
if (this._isStopped)
|
||||
return;
|
||||
if (this._currentTest) {
|
||||
if (this._currentTest && this._currentTest.testInfo._type === 'test') {
|
||||
if (this._currentTest.testInfo.error)
|
||||
return;
|
||||
this._currentTest.testInfo.status = 'failed';
|
||||
|
|
@ -153,7 +153,7 @@ export class WorkerRunner extends EventEmitter {
|
|||
if (!this._fixtureRunner.dependsOnWorkerFixturesOnly(beforeAllModifier.fn, beforeAllModifier.location))
|
||||
continue;
|
||||
// TODO: separate timeout for beforeAll modifiers?
|
||||
const result = await raceAgainstDeadline(this._fixtureRunner.resolveParametersAndRunHookOrTest(beforeAllModifier.fn, 'worker', this._workerInfo), this._deadline());
|
||||
const result = await raceAgainstDeadline(this._fixtureRunner.resolveParametersAndRunHookOrTest(beforeAllModifier.fn, this._workerInfo, undefined), this._deadline());
|
||||
if (result.timedOut) {
|
||||
this._fatalError = serializeError(new Error(`Timeout of ${this._project.config.timeout}ms exceeded while running ${beforeAllModifier.type} modifier`));
|
||||
this._reportDoneAndStop();
|
||||
|
|
@ -162,46 +162,37 @@ export class WorkerRunner extends EventEmitter {
|
|||
annotations.push({ type: beforeAllModifier.type, description: beforeAllModifier.description });
|
||||
}
|
||||
|
||||
const skipHooks = annotations.some(a => a.type === 'fixme' || a.type === 'skip');
|
||||
for (const hook of suite._hooks) {
|
||||
if (hook.type !== 'beforeAll' || skipHooks)
|
||||
for (const hook of suite._allHooks) {
|
||||
if (hook._type !== 'beforeAll')
|
||||
continue;
|
||||
if (this._isStopped)
|
||||
return;
|
||||
// TODO: separate timeout for beforeAll?
|
||||
const result = await raceAgainstDeadline(this._fixtureRunner.resolveParametersAndRunHookOrTest(hook.fn, 'worker', this._workerInfo), this._deadline());
|
||||
if (result.timedOut) {
|
||||
this._fatalError = serializeError(new Error(`Timeout of ${this._project.config.timeout}ms exceeded while running beforeAll hook`));
|
||||
this._reportDoneAndStop();
|
||||
}
|
||||
const firstTest = suite.allTests()[0];
|
||||
await this._runTestOrAllHook(hook, annotations, this._entries.get(firstTest._id)?.retry || 0);
|
||||
}
|
||||
for (const entry of suite._entries) {
|
||||
if (entry instanceof Suite)
|
||||
if (entry instanceof Suite) {
|
||||
await this._runSuite(entry, annotations);
|
||||
else
|
||||
await this._runTest(entry, annotations);
|
||||
} else {
|
||||
const runEntry = this._entries.get(entry._id);
|
||||
if (runEntry)
|
||||
await this._runTestOrAllHook(entry, annotations, runEntry.retry);
|
||||
}
|
||||
}
|
||||
for (const hook of suite._hooks) {
|
||||
if (hook.type !== 'afterAll' || skipHooks)
|
||||
for (const hook of suite._allHooks) {
|
||||
if (hook._type !== 'afterAll')
|
||||
continue;
|
||||
if (this._isStopped)
|
||||
return;
|
||||
// TODO: separate timeout for afterAll?
|
||||
const result = await raceAgainstDeadline(this._fixtureRunner.resolveParametersAndRunHookOrTest(hook.fn, 'worker', this._workerInfo), this._deadline());
|
||||
if (result.timedOut) {
|
||||
this._fatalError = serializeError(new Error(`Timeout of ${this._project.config.timeout}ms exceeded while running afterAll hook`));
|
||||
this._reportDoneAndStop();
|
||||
}
|
||||
await this._runTestOrAllHook(hook, annotations, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private async _runTest(test: TestCase, annotations: Annotations) {
|
||||
private async _runTestOrAllHook(test: TestCase, annotations: Annotations, retry: number) {
|
||||
if (this._isStopped)
|
||||
return;
|
||||
const entry = this._entries.get(test._id);
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
const reportEvents = test._type === 'test';
|
||||
const startTime = monotonicTime();
|
||||
const startWallTime = Date.now();
|
||||
let deadlineRunner: DeadlineRunner<any> | undefined;
|
||||
|
|
@ -213,8 +204,8 @@ export class WorkerRunner extends EventEmitter {
|
|||
let testOutputDir = sanitizedRelativePath + '-' + sanitizeForFilePath(test.title);
|
||||
if (this._uniqueProjectNamePathSegment)
|
||||
testOutputDir += '-' + this._uniqueProjectNamePathSegment;
|
||||
if (entry.retry)
|
||||
testOutputDir += '-retry' + entry.retry;
|
||||
if (retry)
|
||||
testOutputDir += '-retry' + retry;
|
||||
if (this._params.repeatEachIndex)
|
||||
testOutputDir += '-repeat' + this._params.repeatEachIndex;
|
||||
return path.join(this._project.config.outputDir, testOutputDir);
|
||||
|
|
@ -223,14 +214,16 @@ export class WorkerRunner extends EventEmitter {
|
|||
let testFinishedCallback = () => {};
|
||||
let lastStepId = 0;
|
||||
const testInfo: TestInfoImpl = {
|
||||
...this._workerInfo,
|
||||
workerIndex: this._params.workerIndex,
|
||||
project: this._project.config,
|
||||
config: this._loader.fullConfig(),
|
||||
title: test.title,
|
||||
file: test.location.file,
|
||||
line: test.location.line,
|
||||
column: test.location.column,
|
||||
fn: test.fn,
|
||||
repeatEachIndex: this._params.repeatEachIndex,
|
||||
retry: entry.retry,
|
||||
retry,
|
||||
expectedStatus: test.expectedStatus,
|
||||
annotations: [],
|
||||
attachments: [],
|
||||
|
|
@ -277,7 +270,8 @@ export class WorkerRunner extends EventEmitter {
|
|||
title,
|
||||
wallTime: Date.now()
|
||||
};
|
||||
this.emit('stepBegin', payload);
|
||||
if (reportEvents)
|
||||
this.emit('stepBegin', payload);
|
||||
return (error?: Error | TestError) => {
|
||||
if (error instanceof Error)
|
||||
error = serializeError(error);
|
||||
|
|
@ -287,9 +281,11 @@ export class WorkerRunner extends EventEmitter {
|
|||
wallTime: Date.now(),
|
||||
error
|
||||
};
|
||||
this.emit('stepEnd', payload);
|
||||
if (reportEvents)
|
||||
this.emit('stepEnd', payload);
|
||||
};
|
||||
},
|
||||
_type: test._type,
|
||||
};
|
||||
|
||||
// Inherit test.setTimeout() from parent suites.
|
||||
|
|
@ -323,11 +319,13 @@ export class WorkerRunner extends EventEmitter {
|
|||
return testInfo.timeout ? startTime + testInfo.timeout : undefined;
|
||||
};
|
||||
|
||||
this.emit('testBegin', buildTestBeginPayload(testId, testInfo, startWallTime));
|
||||
if (reportEvents)
|
||||
this.emit('testBegin', buildTestBeginPayload(testId, testInfo, startWallTime));
|
||||
|
||||
if (testInfo.expectedStatus === 'skipped') {
|
||||
testInfo.status = 'skipped';
|
||||
this.emit('testEnd', buildTestEndPayload(testId, testInfo));
|
||||
if (reportEvents)
|
||||
this.emit('testEnd', buildTestEndPayload(testId, testInfo));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -360,7 +358,8 @@ export class WorkerRunner extends EventEmitter {
|
|||
}
|
||||
|
||||
testInfo.duration = monotonicTime() - startTime;
|
||||
this.emit('testEnd', buildTestEndPayload(testId, testInfo));
|
||||
if (reportEvents)
|
||||
this.emit('testEnd', buildTestEndPayload(testId, testInfo));
|
||||
|
||||
const isFailure = testInfo.status === 'timedOut' || (testInfo.status === 'failed' && testInfo.expectedStatus !== 'failed');
|
||||
const preserveOutput = this._loader.fullConfig().preserveOutput === 'always' ||
|
||||
|
|
@ -373,7 +372,10 @@ export class WorkerRunner extends EventEmitter {
|
|||
if (this._isStopped)
|
||||
return;
|
||||
if (testInfo.status !== 'passed' && testInfo.status !== 'skipped') {
|
||||
this._failedTestId = testId;
|
||||
if (test._type === 'test')
|
||||
this._failedTestId = testId;
|
||||
else
|
||||
this._fatalError = testInfo.error;
|
||||
this._reportDoneAndStop();
|
||||
}
|
||||
}
|
||||
|
|
@ -383,7 +385,7 @@ export class WorkerRunner extends EventEmitter {
|
|||
setCurrentTestInfo(currentTest ? currentTest.testInfo : null);
|
||||
}
|
||||
|
||||
private async _runTestWithBeforeHooks(test: TestCase, testInfo: TestInfoImpl) {
|
||||
private async _runBeforeHooks(test: TestCase, testInfo: TestInfoImpl) {
|
||||
let completeStep: CompleteStepCallback | undefined;
|
||||
try {
|
||||
const beforeEachModifiers: Modifier[] = [];
|
||||
|
|
@ -395,7 +397,7 @@ export class WorkerRunner extends EventEmitter {
|
|||
for (const modifier of beforeEachModifiers) {
|
||||
if (this._isStopped)
|
||||
return;
|
||||
const result = await this._fixtureRunner.resolveParametersAndRunHookOrTest(modifier.fn, 'test', testInfo);
|
||||
const result = await this._fixtureRunner.resolveParametersAndRunHookOrTest(modifier.fn, this._workerInfo, testInfo);
|
||||
testInfo[modifier.type](!!result, modifier.description!);
|
||||
}
|
||||
completeStep = testInfo._addStep('hook', 'Before Hooks');
|
||||
|
|
@ -411,13 +413,18 @@ export class WorkerRunner extends EventEmitter {
|
|||
// Continue running afterEach hooks even after the failure.
|
||||
}
|
||||
completeStep?.(testInfo.error);
|
||||
}
|
||||
|
||||
private async _runTestWithBeforeHooks(test: TestCase, testInfo: TestInfoImpl) {
|
||||
if (test._type === 'test')
|
||||
await this._runBeforeHooks(test, testInfo);
|
||||
|
||||
// Do not run the test when beforeEach hook fails.
|
||||
if (this._isStopped || testInfo.status === 'failed' || testInfo.status === 'skipped')
|
||||
return;
|
||||
|
||||
try {
|
||||
await this._fixtureRunner.resolveParametersAndRunHookOrTest(test.fn, 'test', testInfo);
|
||||
await this._fixtureRunner.resolveParametersAndRunHookOrTest(test.fn, this._workerInfo, testInfo);
|
||||
} catch (error) {
|
||||
if (error instanceof SkipError) {
|
||||
if (testInfo.status === 'passed')
|
||||
|
|
@ -439,7 +446,8 @@ export class WorkerRunner extends EventEmitter {
|
|||
let teardownError: TestError | undefined;
|
||||
try {
|
||||
completeStep = testInfo._addStep('hook', 'After Hooks');
|
||||
await this._runHooks(test.parent!, 'afterEach', testInfo);
|
||||
if (test._type === 'test')
|
||||
await this._runHooks(test.parent!, 'afterEach', testInfo);
|
||||
} catch (error) {
|
||||
if (!(error instanceof SkipError)) {
|
||||
if (testInfo.status === 'passed')
|
||||
|
|
@ -469,7 +477,7 @@ export class WorkerRunner extends EventEmitter {
|
|||
return;
|
||||
const all = [];
|
||||
for (let s: Suite | undefined = suite; s; s = s.parent) {
|
||||
const funcs = s._hooks.filter(e => e.type === type).map(e => e.fn);
|
||||
const funcs = s._eachHooks.filter(e => e.type === type).map(e => e.fn);
|
||||
all.push(...funcs.reverse());
|
||||
}
|
||||
if (type === 'beforeEach')
|
||||
|
|
@ -477,7 +485,7 @@ export class WorkerRunner extends EventEmitter {
|
|||
let error: Error | undefined;
|
||||
for (const hook of all) {
|
||||
try {
|
||||
await this._fixtureRunner.resolveParametersAndRunHookOrTest(hook, 'test', testInfo);
|
||||
await this._fixtureRunner.resolveParametersAndRunHookOrTest(hook, this._workerInfo, testInfo);
|
||||
} catch (e) {
|
||||
// Always run all the hooks, and capture the first error.
|
||||
error = error || e;
|
||||
|
|
|
|||
|
|
@ -179,44 +179,6 @@ test('should throw when worker fixture depends on a test fixture', async ({ runI
|
|||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test('should throw when beforeAll hook depends on a test fixture', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'f.spec.ts': `
|
||||
const test = pwt.test.extend({
|
||||
foo: [async ({}, runTest) => {
|
||||
await runTest();
|
||||
}, { scope: 'test' }],
|
||||
});
|
||||
|
||||
test.beforeAll(async ({ foo }) => {});
|
||||
test('works', async ({ foo }) => {});
|
||||
`,
|
||||
});
|
||||
expect(result.output).toContain('beforeAll hook cannot depend on a test fixture "foo".');
|
||||
expect(result.output).toContain(`f.spec.ts:11:12`);
|
||||
expect(result.output).toContain(`f.spec.ts:5:29`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test('should throw when afterAll hook depends on a test fixture', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'f.spec.ts': `
|
||||
const test = pwt.test.extend({
|
||||
foo: [async ({}, runTest) => {
|
||||
await runTest();
|
||||
}, { scope: 'test' }],
|
||||
});
|
||||
|
||||
test.afterAll(async ({ foo }) => {});
|
||||
test('works', async ({ foo }) => {});
|
||||
`,
|
||||
});
|
||||
expect(result.output).toContain('afterAll hook cannot depend on a test fixture "foo".');
|
||||
expect(result.output).toContain(`f.spec.ts:11:12`);
|
||||
expect(result.output).toContain(`f.spec.ts:5:29`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test('should define the same fixture in two files', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
|
|
|
|||
|
|
@ -312,27 +312,27 @@ test('automatic fixtures should work', async ({ runInlineTest }) => {
|
|||
});
|
||||
test.beforeAll(async ({}) => {
|
||||
expect(counterWorker).toBe(1);
|
||||
expect(counterTest).toBe(0);
|
||||
expect(counterTest).toBe(1);
|
||||
});
|
||||
test.beforeEach(async ({}) => {
|
||||
expect(counterWorker).toBe(1);
|
||||
expect(counterTest === 1 || counterTest === 2).toBe(true);
|
||||
expect(counterTest === 2 || counterTest === 3).toBe(true);
|
||||
});
|
||||
test('test 1', async ({}) => {
|
||||
expect(counterWorker).toBe(1);
|
||||
expect(counterTest).toBe(1);
|
||||
expect(counterTest).toBe(2);
|
||||
});
|
||||
test('test 2', async ({}) => {
|
||||
expect(counterWorker).toBe(1);
|
||||
expect(counterTest).toBe(2);
|
||||
expect(counterTest).toBe(3);
|
||||
});
|
||||
test.afterEach(async ({}) => {
|
||||
expect(counterWorker).toBe(1);
|
||||
expect(counterTest === 1 || counterTest === 2).toBe(true);
|
||||
expect(counterTest === 2 || counterTest === 3).toBe(true);
|
||||
});
|
||||
test.afterAll(async ({}) => {
|
||||
expect(counterWorker).toBe(1);
|
||||
expect(counterTest).toBe(2);
|
||||
expect(counterTest).toBe(4);
|
||||
});
|
||||
`
|
||||
});
|
||||
|
|
@ -340,6 +340,35 @@ test('automatic fixtures should work', async ({ runInlineTest }) => {
|
|||
expect(result.results.map(r => r.status)).toEqual(['passed', 'passed']);
|
||||
});
|
||||
|
||||
test('automatic fixture should start before regular fixture and teardown after', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.js': `
|
||||
const test = pwt.test;
|
||||
test.use({
|
||||
auto: [ async ({}, runTest) => {
|
||||
console.log('\\n%%auto-setup');
|
||||
await runTest();
|
||||
console.log('\\n%%auto-teardown');
|
||||
}, { auto: true } ],
|
||||
foo: async ({}, runTest) => {
|
||||
console.log('\\n%%foo-setup');
|
||||
await runTest();
|
||||
console.log('\\n%%foo-teardown');
|
||||
},
|
||||
});
|
||||
test('test 1', async ({ foo }) => {
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([
|
||||
'%%auto-setup',
|
||||
'%%foo-setup',
|
||||
'%%foo-teardown',
|
||||
'%%auto-teardown',
|
||||
]);
|
||||
});
|
||||
|
||||
test('automatic fixtures should keep workerInfo after conditional skip', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.js': `
|
||||
|
|
@ -628,3 +657,24 @@ test('should run tests in order', async ({ runInlineTest }) => {
|
|||
'%%test3',
|
||||
]);
|
||||
});
|
||||
|
||||
test('worker fixture should not receive TestInfo', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.js': `
|
||||
const test = pwt.test;
|
||||
test.use({
|
||||
worker: [async ({}, use, info) => {
|
||||
expect(info.title).toBe(undefined);
|
||||
await use();
|
||||
}, { scope: 'worker' }],
|
||||
test: async ({ worker }, use, info) => {
|
||||
expect(info.title).not.toBe(undefined);
|
||||
await use();
|
||||
},
|
||||
});
|
||||
test('test 1', async ({ test }) => {
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ test('hooks should work with fixtures', async ({ runInlineTest }) => {
|
|||
const { results } = await runInlineTest({
|
||||
'helper.ts': `
|
||||
global.logs = [];
|
||||
let counter = 0;
|
||||
export const test = pwt.test.extend({
|
||||
w: [ async ({}, run) => {
|
||||
global.logs.push('+w');
|
||||
|
|
@ -29,7 +30,8 @@ test('hooks should work with fixtures', async ({ runInlineTest }) => {
|
|||
|
||||
t: async ({}, run) => {
|
||||
global.logs.push('+t');
|
||||
await run(42);
|
||||
await run(42 + counter);
|
||||
++counter;
|
||||
global.logs.push('-t');
|
||||
},
|
||||
});
|
||||
|
|
@ -37,36 +39,39 @@ test('hooks should work with fixtures', async ({ runInlineTest }) => {
|
|||
'a.test.js': `
|
||||
const { test } = require('./helper');
|
||||
test.describe('suite', () => {
|
||||
test.beforeAll(async ({ w }) => {
|
||||
global.logs.push('beforeAll-' + w);
|
||||
test.beforeAll(async ({ w, t }) => {
|
||||
global.logs.push('beforeAll-' + w + '-' + t);
|
||||
});
|
||||
test.afterAll(async ({ w }) => {
|
||||
global.logs.push('afterAll-' + w);
|
||||
test.afterAll(async ({ w, t }) => {
|
||||
global.logs.push('afterAll-' + w + '-' + t);
|
||||
});
|
||||
|
||||
test.beforeEach(async ({t}) => {
|
||||
global.logs.push('beforeEach-' + t);
|
||||
test.beforeEach(async ({ w, t }) => {
|
||||
global.logs.push('beforeEach-' + w + '-' + t);
|
||||
});
|
||||
test.afterEach(async ({t}) => {
|
||||
global.logs.push('afterEach-' + t);
|
||||
test.afterEach(async ({ w, t }) => {
|
||||
global.logs.push('afterEach-' + w + '-' + t);
|
||||
});
|
||||
|
||||
test('one', async ({t}) => {
|
||||
global.logs.push('test');
|
||||
expect(t).toBe(42);
|
||||
test('one', async ({ w, t }) => {
|
||||
global.logs.push('test-' + w + '-' + t);
|
||||
});
|
||||
});
|
||||
|
||||
test('two', async ({t}) => {
|
||||
test('two', async ({ t }) => {
|
||||
expect(global.logs).toEqual([
|
||||
'+w',
|
||||
'beforeAll-17',
|
||||
'+t',
|
||||
'beforeEach-42',
|
||||
'test',
|
||||
'afterEach-42',
|
||||
'beforeAll-17-42',
|
||||
'-t',
|
||||
'+t',
|
||||
'beforeEach-17-43',
|
||||
'test-17-43',
|
||||
'afterEach-17-43',
|
||||
'-t',
|
||||
'+t',
|
||||
'afterAll-17-44',
|
||||
'-t',
|
||||
'afterAll-17',
|
||||
'+t',
|
||||
]);
|
||||
});
|
||||
|
|
@ -214,3 +219,26 @@ test('beforeAll hooks are skipped when no tests in the suite are run', async ({
|
|||
expect(result.output).toContain('%%beforeAll2');
|
||||
expect(result.output).not.toContain('%%beforeAll1');
|
||||
});
|
||||
|
||||
test('beforeAll hook should get retry index of the first test', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.js': `
|
||||
const { test } = pwt;
|
||||
test.beforeAll(({}, testInfo) => {
|
||||
console.log('\\n%%beforeall-retry-' + testInfo.retry);
|
||||
});
|
||||
test('passed', ({}, testInfo) => {
|
||||
console.log('\\n%%test-retry-' + testInfo.retry);
|
||||
expect(testInfo.retry).toBe(1);
|
||||
});
|
||||
`,
|
||||
}, { retries: 1 });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.flaky).toBe(1);
|
||||
expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([
|
||||
'%%beforeall-retry-0',
|
||||
'%%test-retry-0',
|
||||
'%%beforeall-retry-1',
|
||||
'%%test-retry-1',
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ test('basics should work', async ({runTSC}) => {
|
|||
testInfo.annotations[0].type;
|
||||
});
|
||||
});
|
||||
// @ts-expect-error
|
||||
test.foo();
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
|
|
|||
|
|
@ -16,17 +16,6 @@
|
|||
|
||||
import { test, expect } from './playwright-test-fixtures';
|
||||
|
||||
test('sanity', async ({runTSC}) => {
|
||||
const result = await runTSC({
|
||||
'a.spec.ts': `
|
||||
const { test } = pwt;
|
||||
// @ts-expect-error
|
||||
test.foo();
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should check types of fixtures', async ({runTSC}) => {
|
||||
const result = await runTSC({
|
||||
'helper.ts': `
|
||||
|
|
@ -125,9 +114,7 @@ test('should check types of fixtures', async ({runTSC}) => {
|
|||
|
||||
// @ts-expect-error
|
||||
test.beforeAll(async ({ a }) => {});
|
||||
// @ts-expect-error
|
||||
test.beforeAll(async ({ foo, bar }) => {});
|
||||
test.beforeAll(async ({ bar }) => {});
|
||||
test.beforeAll(() => {});
|
||||
|
||||
// @ts-expect-error
|
||||
|
|
@ -137,9 +124,7 @@ test('should check types of fixtures', async ({runTSC}) => {
|
|||
|
||||
// @ts-expect-error
|
||||
test.afterAll(async ({ a }) => {});
|
||||
// @ts-expect-error
|
||||
test.afterAll(async ({ foo, bar }) => {});
|
||||
test.afterAll(async ({ bar }) => {});
|
||||
test.afterAll(() => {});
|
||||
`
|
||||
});
|
||||
|
|
|
|||
8
types/test.d.ts
vendored
8
types/test.d.ts
vendored
|
|
@ -2024,17 +2024,17 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
|
|||
*
|
||||
* You can use [test.afterAll(hookFunction)](https://playwright.dev/docs/api/class-test#test-after-all) to teardown any
|
||||
* resources set up in `beforeAll`.
|
||||
* @param hookFunction Hook function that takes one or two arguments: an object with fixtures and optional [WorkerInfo].
|
||||
* @param hookFunction Hook function that takes one or two arguments: an object with fixtures and optional [TestInfo].
|
||||
*/
|
||||
beforeAll(inner: (args: WorkerArgs, workerInfo: WorkerInfo) => Promise<any> | any): void;
|
||||
beforeAll(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
|
||||
/**
|
||||
* Declares an `afterAll` hook that is executed once after all tests. When called in the scope of a test file, runs after
|
||||
* all tests in the file. When called inside a
|
||||
* [test.describe(title, callback)](https://playwright.dev/docs/api/class-test#test-describe) group, runs after all tests
|
||||
* in the group.
|
||||
* @param hookFunction Hook function that takes one or two arguments: an object with fixtures and optional [WorkerInfo].
|
||||
* @param hookFunction Hook function that takes one or two arguments: an object with fixtures and optional [TestInfo].
|
||||
*/
|
||||
afterAll(inner: (args: WorkerArgs, workerInfo: WorkerInfo) => Promise<any> | any): void;
|
||||
afterAll(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
|
||||
/**
|
||||
* Specifies parameters or fixtures to use in a single test file or a
|
||||
* [test.describe(title, callback)](https://playwright.dev/docs/api/class-test#test-describe) group. Most useful to
|
||||
|
|
|
|||
4
utils/generate_types/overrides-test.d.ts
vendored
4
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -247,8 +247,8 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
|
|||
setTimeout(timeout: number): void;
|
||||
beforeEach(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
|
||||
afterEach(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
|
||||
beforeAll(inner: (args: WorkerArgs, workerInfo: WorkerInfo) => Promise<any> | any): void;
|
||||
afterAll(inner: (args: WorkerArgs, workerInfo: WorkerInfo) => Promise<any> | any): void;
|
||||
beforeAll(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
|
||||
afterAll(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
|
||||
use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void;
|
||||
step(title: string, body: () => Promise<any>): Promise<any>;
|
||||
expect: Expect;
|
||||
|
|
|
|||
Loading…
Reference in a new issue