From 81a7c8ad6b58d10ed6d7cb084ca0641aa30c528c Mon Sep 17 00:00:00 2001 From: oilater Date: Sat, 14 Mar 2026 01:55:52 +0900 Subject: [PATCH 1/2] fix: detect fixture that returns without calling `use` (#9831) Instead of waiting for a timeout, immediately reject with a descriptive error when a fixture function returns without calling `use()`. --- packages/runner/src/fixture.ts | 9 +++++++++ .../test-extend/fixture-without-use.test.ts | 20 +++++++++++++++++++ .../cli/test/__snapshots__/fails.test.ts.snap | 2 ++ 3 files changed, 31 insertions(+) create mode 100644 test/cli/fixtures/fails/test-extend/fixture-without-use.test.ts diff --git a/packages/runner/src/fixture.ts b/packages/runner/src/fixture.ts index 002bb7bc6063..c829e4d94bc9 100644 --- a/packages/runner/src/fixture.ts +++ b/packages/runner/src/fixture.ts @@ -500,6 +500,15 @@ async function resolveFixtureFunction( await fixtureReturn }) await useReturnPromise + }).then(() => { + // fixture returned without calling use() + if (!isUseFnArgResolved) { + useFnArgPromise.reject( + new Error( + 'Fixture returned without calling "use". Make sure to call "use" in every code path of the fixture function.', + ), + ) + } }).catch((e: unknown) => { // treat fixture setup error as test failure if (!isUseFnArgResolved) { diff --git a/test/cli/fixtures/fails/test-extend/fixture-without-use.test.ts b/test/cli/fixtures/fails/test-extend/fixture-without-use.test.ts new file mode 100644 index 000000000000..5bc407cf0177 --- /dev/null +++ b/test/cli/fixtures/fails/test-extend/fixture-without-use.test.ts @@ -0,0 +1,20 @@ +import { describe, test as base } from 'vitest' + +const test = base.extend<{ value: string | undefined, setup: void }>({ + value: undefined, + + setup: [ + async ({ value }, use) => { + if (!value) { + return + } + + await use(undefined) + }, + { auto: true }, + ], +}) + +describe('fixture returned without calling use', () => { + test('should fail with descriptive error', () => {}) +}) diff --git a/test/cli/test/__snapshots__/fails.test.ts.snap b/test/cli/test/__snapshots__/fails.test.ts.snap index 74303db30f83..9edef1500737 100644 --- a/test/cli/test/__snapshots__/fails.test.ts.snap +++ b/test/cli/test/__snapshots__/fails.test.ts.snap @@ -91,6 +91,8 @@ exports[`should fail test-extend/fixture-rest-props.test.ts 1`] = `"FixtureParse exports[`should fail test-extend/fixture-without-destructuring.test.ts 1`] = `"FixtureParseError: The 1st argument inside a fixture must use object destructuring pattern, e.g. ({ task } => {}). Instead, received "context"."`; +exports[`should fail test-extend/fixture-without-use.test.ts 1`] = `"Error: Fixture returned without calling "use". Make sure to call "use" in every code path of the fixture function."`; + exports[`should fail test-extend/test-rest-params.test.ts 1`] = `"FixtureParseError: The 1st argument inside a fixture must use object destructuring pattern, e.g. ({ task } => {}). Instead, received "...rest"."`; exports[`should fail test-extend/test-rest-props.test.ts 1`] = `"FixtureParseError: Rest parameters are not supported in fixtures, received "...rest"."`; From 09b3f1ca5b41c9de4f80413f1c8e127f13c33647 Mon Sep 17 00:00:00 2001 From: oilater Date: Mon, 16 Mar 2026 12:56:54 +0900 Subject: [PATCH 2/2] fix: add stack trace and fixture name to "use not called" error - Capture registration-time stack trace using Symbol-based pattern from hooks.ts - Include fixture name in error message for easier identification - Move test to runInlineTests + stderr inline snapshot pattern --- packages/runner/src/fixture.ts | 23 ++++++++-- .../test-extend/fixture-without-use.test.ts | 20 --------- .../cli/test/__snapshots__/fails.test.ts.snap | 2 - test/cli/test/scoped-fixtures.test.ts | 42 +++++++++++++++++++ 4 files changed, 61 insertions(+), 26 deletions(-) delete mode 100644 test/cli/fixtures/fails/test-extend/fixture-without-use.test.ts diff --git a/packages/runner/src/fixture.ts b/packages/runner/src/fixture.ts index c829e4d94bc9..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) => { @@ -503,11 +516,13 @@ async function resolveFixtureFunction( }).then(() => { // fixture returned without calling use() if (!isUseFnArgResolved) { - useFnArgPromise.reject( - new Error( - 'Fixture returned without calling "use". Make sure to call "use" in every code path of the fixture function.', - ), + 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 diff --git a/test/cli/fixtures/fails/test-extend/fixture-without-use.test.ts b/test/cli/fixtures/fails/test-extend/fixture-without-use.test.ts deleted file mode 100644 index 5bc407cf0177..000000000000 --- a/test/cli/fixtures/fails/test-extend/fixture-without-use.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, test as base } from 'vitest' - -const test = base.extend<{ value: string | undefined, setup: void }>({ - value: undefined, - - setup: [ - async ({ value }, use) => { - if (!value) { - return - } - - await use(undefined) - }, - { auto: true }, - ], -}) - -describe('fixture returned without calling use', () => { - test('should fail with descriptive error', () => {}) -}) diff --git a/test/cli/test/__snapshots__/fails.test.ts.snap b/test/cli/test/__snapshots__/fails.test.ts.snap index 9edef1500737..74303db30f83 100644 --- a/test/cli/test/__snapshots__/fails.test.ts.snap +++ b/test/cli/test/__snapshots__/fails.test.ts.snap @@ -91,8 +91,6 @@ exports[`should fail test-extend/fixture-rest-props.test.ts 1`] = `"FixtureParse exports[`should fail test-extend/fixture-without-destructuring.test.ts 1`] = `"FixtureParseError: The 1st argument inside a fixture must use object destructuring pattern, e.g. ({ task } => {}). Instead, received "context"."`; -exports[`should fail test-extend/fixture-without-use.test.ts 1`] = `"Error: Fixture returned without calling "use". Make sure to call "use" in every code path of the fixture function."`; - exports[`should fail test-extend/test-rest-params.test.ts 1`] = `"FixtureParseError: The 1st argument inside a fixture must use object destructuring pattern, e.g. ({ task } => {}). Instead, received "...rest"."`; exports[`should fail test-extend/test-rest-props.test.ts 1`] = `"FixtureParseError: Rest parameters are not supported in fixtures, received "...rest"."`; 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