diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index 8f6012fd63d8..340dec122c0a 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -1128,6 +1128,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { promise, createAssertionMessage(utils, this, !!args.length), error, + utils.flag(this, 'soft'), ) } }, @@ -1203,6 +1204,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { promise, createAssertionMessage(utils, this, !!args.length), error, + utils.flag(this, 'soft'), ) } }, 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/fixtures/expect-soft/expects/soft.test.ts b/test/cli/fixtures/expect-soft/expects/soft.test.ts index 6d72e483cbb9..e346e588366f 100644 --- a/test/cli/fixtures/expect-soft/expects/soft.test.ts +++ b/test/cli/fixtures/expect-soft/expects/soft.test.ts @@ -75,6 +75,29 @@ test('promise with expect.extend', async () => { await expect.soft(2 + 2).toBeAsync(3); }); +test('promise rejection', async () => { + await expect + .soft( + Promise.resolve().then(() => { + throw new Error('boom 1st') + }), + ) + .resolves.toBe('1st') + + await expect + .soft( + Promise.resolve().then(() => { + throw new Error('boom 2nd') + }), + ) + .resolves.toBe('2nd') +}) + +test('promise resolved instead of rejecting', async () => { + await expect.soft(Promise.resolve('value 1')).rejects.toBe('1st') + await expect.soft(Promise.resolve('value 2')).rejects.toBe('2nd') +}) + test('passed', () => { expect.soft(1).toEqual(1) expect(10).toEqual(10) diff --git a/test/cli/test/expect-soft.test.ts b/test/cli/test/expect-soft.test.ts index 49355621686c..cbc04b8063eb 100644 --- a/test/cli/test/expect-soft.test.ts +++ b/test/cli/test/expect-soft.test.ts @@ -45,6 +45,20 @@ describe('expect.soft', () => { expect.soft(stderr).toContain('Error: expected 4 to be 3') }) + test('promise rejection', async () => { + const { stderr } = await run() + // both assertions should execute (not abort after first rejection) + expect.soft(stderr).toContain('promise rejected "Error: boom 1st" instead of resolving') + expect.soft(stderr).toContain('promise rejected "Error: boom 2nd" instead of resolving') + }) + + test('promise resolved instead of rejecting', async () => { + const { stderr } = await run() + // both assertions should execute + expect.soft(stderr).toContain('promise resolved "\'value 1\'" instead of rejecting') + expect.soft(stderr).toContain('promise resolved "\'value 2\'" instead of rejecting') + }) + test('passed', async () => { const { stdout } = await run() expect.soft(stdout).toContain('soft.test.ts > passed') 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