[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix(snapshot): fail test when snapshot assertion is used with `test.fails` (#10090)

authored by

Hiroshi Ogawa and committed by
GitHub
(Apr 21, 2026, 1:27 PM +0200) e1aa7a60 6c3e4bdb

+118 -18
+16
packages/runner/src/errors.ts
··· 43 43 export class AroundHookMultipleCallsError extends Error { 44 44 public name = 'AroundHookMultipleCallsError' 45 45 } 46 + 47 + // `test.fails` doesn't flip the test result when this error is thrown 48 + export class TestSyntaxError extends Error { 49 + public name = 'TestSyntaxError' 50 + 51 + constructor(message: string) { 52 + super(message) 53 + // use custom property so this survives when the error 54 + // is serialized on `packages/expect` side (e.g. for `expect.soft`) 55 + // and `packages/runner` can still detect it during `test.fails` handling 56 + Object.defineProperty(this, '__vitest_test_syntax_error__', { 57 + value: true, 58 + enumerable: false, 59 + }) 60 + } 61 + }
+1
packages/runner/src/index.ts
··· 1 1 export { recordArtifact } from './artifact' 2 + export { TestSyntaxError } from './errors' 2 3 export { 3 4 afterAll, 4 5 afterEach,
+2 -2
packages/runner/src/run.ts
··· 761 761 } 762 762 } 763 763 764 - // if test is marked to be failed, flip the result 764 + // if test is marked to be failed, flip the result unless `TestSyntaxError` is present 765 765 if (test.fails) { 766 766 if (test.result.state === 'pass') { 767 767 const error = processError(new Error('Expect test to fail')) 768 768 test.result.state = 'fail' 769 769 test.result.errors = [error] 770 770 } 771 - else { 771 + else if (!test.result.errors?.some(e => e.__vitest_test_syntax_error__)) { 772 772 test.result.state = 'pass' 773 773 test.result.errors = undefined 774 774 }
-13
test/core/test/snapshot.test.ts
··· 92 92 }) 93 93 }) 94 94 95 - test.fails('properties snapshot fails', () => { 96 - const user = { 97 - createdAt: new Date(), 98 - id: Math.floor(Math.random() * 20), 99 - name: 'LeBron James', 100 - } 101 - 102 - expect(user).toMatchSnapshot({ 103 - createdAt: expect.any(Date), 104 - id: expect.any(String), 105 - }) 106 - }) 107 - 108 95 test('renders mock snapshot', () => { 109 96 const fn = vi.fn() 110 97 expect(fn).toMatchSnapshot()
+92 -1
test/snapshots/test/snapshots.test.ts
··· 1 1 import { expect, test } from 'vitest' 2 2 3 - import { editFile, runVitest } from '../../test-utils' 3 + import { editFile, runInlineTests, runVitest } from '../../test-utils' 4 4 5 5 test('non default snapshot format', () => { 6 6 expect({ foo: ['bar'] }).toMatchInlineSnapshot(` ··· 26 26 }) 27 27 expect.soft(stdout).include('Snapshots 1 updated') 28 28 expect.soft(exitCode).toBe(0) 29 + }) 30 + 31 + test('test.fails fails snapshot', async () => { 32 + const result = await runInlineTests({ 33 + 'basic.test.ts': ` 34 + import { expect, test } from 'vitest' 35 + 36 + test.fails('file', () => { 37 + expect('a').toMatchSnapshot() 38 + }) 39 + 40 + test.fails('inline', () => { 41 + expect('b').toMatchInlineSnapshot() 42 + }) 43 + 44 + test.fails('soft', () => { 45 + expect.soft('c').toMatchSnapshot() 46 + expect.soft('d').toMatchInlineSnapshot() 47 + }) 48 + `, 49 + }) 50 + expect(result.stderr).toMatchInlineSnapshot(` 51 + " 52 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯ 53 + 54 + FAIL basic.test.ts > file 55 + TestSyntaxError: 'toMatchSnapshot' cannot be used with 'test.fails' 56 + ❯ basic.test.ts:5:15 57 + 3| 58 + 4| test.fails('file', () => { 59 + 5| expect('a').toMatchSnapshot() 60 + | ^ 61 + 6| }) 62 + 7| 63 + 64 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/4]⎯ 65 + 66 + FAIL basic.test.ts > inline 67 + TestSyntaxError: 'toMatchInlineSnapshot' cannot be used with 'test.fails' 68 + ❯ basic.test.ts:9:15 69 + 7| 70 + 8| test.fails('inline', () => { 71 + 9| expect('b').toMatchInlineSnapshot() 72 + | ^ 73 + 10| }) 74 + 11| 75 + 76 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/4]⎯ 77 + 78 + FAIL basic.test.ts > soft 79 + TestSyntaxError: 'toMatchSnapshot' cannot be used with 'test.fails' 80 + ❯ basic.test.ts:13:20 81 + 11| 82 + 12| test.fails('soft', () => { 83 + 13| expect.soft('c').toMatchSnapshot() 84 + | ^ 85 + 14| expect.soft('d').toMatchInlineSnapshot() 86 + 15| }) 87 + 88 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/4]⎯ 89 + 90 + FAIL basic.test.ts > soft 91 + TestSyntaxError: 'toMatchInlineSnapshot' cannot be used with 'test.fails' 92 + ❯ basic.test.ts:14:20 93 + 12| test.fails('soft', () => { 94 + 13| expect.soft('c').toMatchSnapshot() 95 + 14| expect.soft('d').toMatchInlineSnapshot() 96 + | ^ 97 + 15| }) 98 + 16| 99 + 100 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/4]⎯ 101 + 102 + " 103 + `) 104 + expect(result.errorTree()).toMatchInlineSnapshot(` 105 + Object { 106 + "basic.test.ts": Object { 107 + "file": Array [ 108 + "'toMatchSnapshot' cannot be used with 'test.fails'", 109 + ], 110 + "inline": Array [ 111 + "'toMatchInlineSnapshot' cannot be used with 'test.fails'", 112 + ], 113 + "soft": Array [ 114 + "'toMatchSnapshot' cannot be used with 'test.fails'", 115 + "'toMatchInlineSnapshot' cannot be used with 'test.fails'", 116 + ], 117 + }, 118 + } 119 + `) 29 120 })
+1
packages/vitest/src/node/printError.ts
··· 284 284 'VITEST_TEST_PATH', 285 285 '__vitest_rollup_error__', 286 286 '__vitest_error_context__', 287 + '__vitest_test_syntax_error__', 287 288 ...Object.getOwnPropertyNames(Error.prototype), 288 289 ...Object.getOwnPropertyNames(Object.prototype), 289 290 ])
+6 -2
packages/vitest/src/integrations/snapshot/chai.ts
··· 2 2 import type { Test } from '@vitest/runner' 3 3 import type { DomainSnapshotAdapter } from '@vitest/snapshot' 4 4 import { chai, createAssertionMessage, equals, iterableEquality, recordAsyncExpect, subsetEquality, wrapAssertion } from '@vitest/expect' 5 + import { TestSyntaxError } from '@vitest/runner' 5 6 import { getNames } from '@vitest/runner/utils' 6 7 import { 7 8 addSerializer, ··· 62 63 } 63 64 64 65 function getTest(obj: Chai.Assertion) { 65 - const test = chai.util.flag(obj, 'vitest-test') 66 + const test = chai.util.flag(obj, 'vitest-test') as Test | undefined 66 67 if (!test) { 67 68 throw new Error(`'${getAssertionName(obj)}' cannot be used without test context`) 68 69 } 69 - return test as Test 70 + if (test.fails) { 71 + throw new TestSyntaxError(`'${getAssertionName(obj)}' cannot be used with 'test.fails'`) 72 + } 73 + return test 70 74 } 71 75 72 76 function validateAssertion(assertion: Chai.Assertion): void {