diff --git a/packages/runner/src/fixture.ts b/packages/runner/src/fixture.ts index 002bb7bc6063..890f9bde8574 100644 --- a/packages/runner/src/fixture.ts +++ b/packages/runner/src/fixture.ts @@ -5,6 +5,8 @@ import { FixtureAccessError, FixtureDependencyError, FixtureParseError } from '. import { getTestFixtures } from './map' import { getCurrentSuite } from './suite' +const FIXTURE_STACK_TRACE_KEY = Symbol.for('VITEST_FIXTURE_STACK_TRACE') + export interface TestFixtureItem extends FixtureOptions { name: string value: unknown @@ -187,6 +189,10 @@ export class TestFixtures { parent, } + if (isFixtureFunction(value)) { + Object.assign(value, { [FIXTURE_STACK_TRACE_KEY]: new Error('STACK_TRACE_ERROR') }) + } + registrations.set(name, item) if (item.scope === 'worker' && (runner.pool === 'vmThreads' || runner.pool === 'vmForks')) { @@ -427,6 +433,7 @@ function resolveTestFixtureValue( return resolveFixtureFunction( fixture.value, + fixture.name, context, cleanupFnArray, ) @@ -463,6 +470,7 @@ async function resolveScopeFixtureValue( const promise = resolveFixtureFunction( fixture.value, + fixture.name, fixture.scope === 'file' ? { ...workerContext, ...fileContext } : fixtureContext, cleanupFnFileArray, ).then((value) => { @@ -479,11 +487,16 @@ async function resolveFixtureFunction( context: unknown, useFn: (arg: unknown) => Promise, ) => Promise, + fixtureName: string, context: unknown, cleanupFnArray: (() => void | Promise)[], ): Promise { // wait for `use` call to extract fixture value const useFnArgPromise = createDefer() + const stackTraceError + = FIXTURE_STACK_TRACE_KEY in fixtureFn && fixtureFn[FIXTURE_STACK_TRACE_KEY] instanceof Error + ? fixtureFn[FIXTURE_STACK_TRACE_KEY] + : undefined let isUseFnArgResolved = false const fixtureReturn = fixtureFn(context, async (useFnArg: unknown) => { @@ -500,6 +513,17 @@ async function resolveFixtureFunction( await fixtureReturn }) await useReturnPromise + }).then(() => { + // fixture returned without calling use() + if (!isUseFnArgResolved) { + const error = new Error( + `Fixture "${fixtureName}" returned without calling "use". Make sure to call "use" in every code path of the fixture function.`, + ) + if (stackTraceError?.stack) { + error.stack = error.message + stackTraceError.stack.replace(stackTraceError.message, '') + } + useFnArgPromise.reject(error) + } }).catch((e: unknown) => { // treat fixture setup error as test failure if (!isUseFnArgResolved) { diff --git a/test/cli/test/scoped-fixtures.test.ts b/test/cli/test/scoped-fixtures.test.ts index 73511b7f1844..52d7d95b887d 100644 --- a/test/cli/test/scoped-fixtures.test.ts +++ b/test/cli/test/scoped-fixtures.test.ts @@ -53,6 +53,48 @@ test('test fixture cannot import from file fixture', async () => { `) }) +test('fixture returned without calling use', async () => { + const { stderr } = await runInlineTests({ + 'basic.test.ts': () => { + const extendedTest = it.extend<{ + value: string | undefined + setup: void + }>({ + value: undefined, + setup: [ + async ({ value }, use) => { + if (!value) { + return + } + await use(undefined) + }, + { auto: true }, + ], + }) + + extendedTest('should fail with descriptive error', () => {}) + }, + }, { globals: true }) + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL basic.test.ts > should fail with descriptive error + Error: Fixture "setup" returned without calling "use". Make sure to call "use" in every code path of the fixture function. + ❯ basic.test.ts:2:27 + 1| await (() => { + 2| const extendedTest = it.extend({ + | ^ + 3| value: void 0, + 4| setup: [ + ❯ basic.test.ts:16:1 + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) +}) + test('can import file fixture inside the local fixture', async () => { const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => it.extend<{ file: string