From b46594e9fc93a2de64c4f52392da0f58526166c6 Mon Sep 17 00:00:00 2001 From: Ichizo Umehara Date: Wed, 10 Dec 2025 09:10:31 -0500 Subject: [PATCH 01/32] fix: add expect.soft support to toMatchSnapshot (#8673) - toMatchSnapshot, matchSnapshot, toMatchFileSnapshot, toThrowErrorMatchingSnapshot can be used with expect.soft - toMatchInlineSnapshot, toThrowErrorMatchingInlineSnapshot do not support expect.soft and throw an error if used with expect.soft --- .../vitest/src/integrations/snapshot/chai.ts | 80 +++++++++++-------- .../__snapshots__/custom/custom_snapshot1 | 3 + .../__snapshots__/custom/custom_snapshot2 | 3 + .../__snapshots__/snapshot-soft.test.ts.snap | 25 ++++++ test/core/test/snapshot-soft.test.ts | 79 ++++++++++++++++++ 5 files changed, 155 insertions(+), 35 deletions(-) create mode 100644 test/core/test/__snapshots__/custom/custom_snapshot1 create mode 100644 test/core/test/__snapshots__/custom/custom_snapshot2 create mode 100644 test/core/test/__snapshots__/snapshot-soft.test.ts.snap create mode 100644 test/core/test/snapshot-soft.test.ts diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 27bd424e4fef..aea6241ad441 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -7,7 +7,7 @@ import { SnapshotClient, stripSnapshotIndentation, } from '@vitest/snapshot' -import { createAssertionMessage, recordAsyncExpect } from '../../../../expect/src/utils' +import { createAssertionMessage, recordAsyncExpect, wrapAssertion } from '../../../../expect/src/utils' let _client: SnapshotClient @@ -62,42 +62,38 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { } for (const key of ['matchSnapshot', 'toMatchSnapshot']) { - utils.addMethod( - chai.Assertion.prototype, - key, - function ( - this: Record, - properties?: object, - message?: string, - ) { - utils.flag(this, '_name', key) - const isNot = utils.flag(this, 'negate') - if (isNot) { - throw new Error(`${key} cannot be used with "not"`) - } - const expected = utils.flag(this, 'object') - const test = getTest(key, this) - if (typeof properties === 'string' && typeof message === 'undefined') { - message = properties - properties = undefined - } - const errorMessage = utils.flag(this, 'message') - getSnapshotClient().assert({ - received: expected, - message, - isInline: false, - properties, - errorMessage, - ...getTestNames(test), - }) - }, - ) + utils.addMethod(chai.Assertion.prototype, key, wrapAssertion(utils, key, function ( + this: Chai.AssertionStatic & Assertion, + properties?: object, + message?: string, + ) { + utils.flag(this, '_name', key) + const isNot = utils.flag(this, 'negate') + if (isNot) { + throw new Error(`${key} cannot be used with "not"`) + } + const expected = utils.flag(this, 'object') + const test = getTest(key, this) + if (typeof properties === 'string' && typeof message === 'undefined') { + message = properties + properties = undefined + } + const errorMessage = utils.flag(this, 'message') + getSnapshotClient().assert({ + received: expected, + message, + isInline: false, + properties, + errorMessage, + ...getTestNames(test), + }) + })) } utils.addMethod( chai.Assertion.prototype, 'toMatchFileSnapshot', - function (this: Assertion, file: string, message?: string) { + wrapAssertion(utils, 'toMatchFileSnapshot', function (this: Assertion, file: string, message?: string) { utils.flag(this, '_name', 'toMatchFileSnapshot') const isNot = utils.flag(this, 'negate') if (isNot) { @@ -119,13 +115,19 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { ...getTestNames(test), }) + const isSoft = utils.flag(this, 'soft') + if (isSoft) { + // assert result with soft wrapAssertion instead of recordAsyncExpect + return promise + } + return recordAsyncExpect( test, promise, createAssertionMessage(utils, this, true), error, ) - }, + }), ) utils.addMethod( @@ -142,6 +144,10 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { if (isNot) { throw new Error('toMatchInlineSnapshot cannot be used with "not"') } + const isSoft = utils.flag(this, 'soft') + if (isSoft) { + throw new Error('toMatchInlineSnapshot cannot be used with "soft"') + } const test = getTest('toMatchInlineSnapshot', this) const isInsideEach = test.each || test.suite?.each if (isInsideEach) { @@ -176,7 +182,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { utils.addMethod( chai.Assertion.prototype, 'toThrowErrorMatchingSnapshot', - function (this: Record, message?: string) { + wrapAssertion(utils, 'toThrowErrorMatchingSnapshot', function (this: Chai.AssertionStatic & Assertion, properties?: object, message?: string) { utils.flag(this, '_name', 'toThrowErrorMatchingSnapshot') const isNot = utils.flag(this, 'negate') if (isNot) { @@ -194,7 +200,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { errorMessage, ...getTestNames(test), }) - }, + }), ) utils.addMethod( chai.Assertion.prototype, @@ -210,6 +216,10 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { 'toThrowErrorMatchingInlineSnapshot cannot be used with "not"', ) } + const isSoft = utils.flag(this, 'soft') + if (isSoft) { + throw new Error('toThrowErrorMatchingInlineSnapshot cannot be used with "soft"') + } const test = getTest('toThrowErrorMatchingInlineSnapshot', this) const isInsideEach = test.each || test.suite?.each if (isInsideEach) { diff --git a/test/core/test/__snapshots__/custom/custom_snapshot1 b/test/core/test/__snapshots__/custom/custom_snapshot1 new file mode 100644 index 000000000000..d88d04d0711a --- /dev/null +++ b/test/core/test/__snapshots__/custom/custom_snapshot1 @@ -0,0 +1,3 @@ +{ + "time": 2025-12-11T05:21:52.329Z, +} \ No newline at end of file diff --git a/test/core/test/__snapshots__/custom/custom_snapshot2 b/test/core/test/__snapshots__/custom/custom_snapshot2 new file mode 100644 index 000000000000..7513d971361e --- /dev/null +++ b/test/core/test/__snapshots__/custom/custom_snapshot2 @@ -0,0 +1,3 @@ +{ + "uuid": "e406eb00-df24-4070-8742-0c3bc05de89d", +} \ No newline at end of file diff --git a/test/core/test/__snapshots__/snapshot-soft.test.ts.snap b/test/core/test/__snapshots__/snapshot-soft.test.ts.snap new file mode 100644 index 000000000000..b64f7f849614 --- /dev/null +++ b/test/core/test/__snapshots__/snapshot-soft.test.ts.snap @@ -0,0 +1,25 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`expect.soft tracks failures for matchSnapshot 1`] = ` +{ + "time": "2000-01-01T00:00:00.000Z", +} +`; + +exports[`expect.soft tracks failures for matchSnapshot 2`] = ` +{ + "uuid": "00000000-0000-0000-0000-000000000000", +} +`; + +exports[`expect.soft tracks failures for toMatchSnapshot 1`] = ` +{ + "time": "2000-01-01T00:00:00.000Z", +} +`; + +exports[`expect.soft tracks failures for toMatchSnapshot 2`] = ` +{ + "uuid": "00000000-0000-0000-0000-000000000000", +} +`; diff --git a/test/core/test/snapshot-soft.test.ts b/test/core/test/snapshot-soft.test.ts new file mode 100644 index 000000000000..fcbba3b9526f --- /dev/null +++ b/test/core/test/snapshot-soft.test.ts @@ -0,0 +1,79 @@ +import { getCurrentTest } from '@vitest/runner' +import { expect, test } from 'vitest' + +test('expect.soft tracks failures for matchSnapshot', () => { + expect.soft({ time: new Date() }).matchSnapshot() + expect.soft({ uuid: crypto.randomUUID() }).matchSnapshot() + + expect(getCurrentTest()?.result?.errors).toHaveLength(2) + resetFailingTestState() + resetUnmatchedSnapshotFailures() +}) + +test('expect.soft tracks failures for toMatchSnapshot', async () => { + expect.soft({ time: new Date() }).toMatchSnapshot() + expect.soft({ uuid: crypto.randomUUID() }).toMatchSnapshot() + + expect(getCurrentTest()?.result?.errors).toHaveLength(2) + resetFailingTestState() + resetUnmatchedSnapshotFailures() +}) + +test('expect.soft tracks failures for toMatchFileSnapshot', async () => { + await expect.soft({ time: new Date() }).toMatchFileSnapshot('./__snapshots__/custom/custom_snapshot1') + await expect.soft({ uuid: crypto.randomUUID() }).toMatchFileSnapshot('./__snapshots__/custom/custom_snapshot2') + + expect(getCurrentTest()?.result?.errors).toHaveLength(2) + resetFailingTestState() + resetUnmatchedSnapshotFailures() +}) + +test('expect.soft tracks failures for toThrowErrorMatchingSnapshot', () => { + expect.soft({ time: '2011-11-11' }).toThrowErrorMatchingSnapshot() + expect.soft({ uuid: 'eb10615a-e798-4630-8209-4ab082345e78' }).toThrowErrorMatchingSnapshot() + + expect(getCurrentTest()?.result?.errors).toHaveLength(2) + resetFailingTestState() + resetUnmatchedSnapshotFailures() +}) + +test('expect.soft fails if run with toMatchInlineSnapshot', () => { + let exceptionThrown = false + try { + expect.soft(1).toMatchInlineSnapshot(`1`) + } + catch (error: any) { + expect(error?.message).toContain('toMatchInlineSnapshot cannot be used with "soft"') + exceptionThrown = true + } + + expect(exceptionThrown).toBe(true) + resetUnmatchedSnapshotFailures() +}) + +test('expect.soft fails if run with toThrowErrorMatchingInlineSnapshot', () => { + let exceptionThrown = false + try { + expect.soft(() => { + throw new Error('1') + }).toThrowErrorMatchingInlineSnapshot(`[Error: 2]`) + } + catch (error: any) { + expect(error?.message).toContain('toThrowErrorMatchingInlineSnapshot cannot be used with "soft"') + exceptionThrown = true + } + + expect(exceptionThrown).toBe(true) + resetUnmatchedSnapshotFailures() +}) + +function resetFailingTestState() { + getCurrentTest()!.result!.state = 'run' +} + +function resetUnmatchedSnapshotFailures() { + const snapshotState = expect.getState().snapshotState + if (snapshotState?.unmatched) { + snapshotState.unmatched = 0 + } +} From 81bddb7c9b8cc142fe276a065a1b2d7251825b49 Mon Sep 17 00:00:00 2001 From: Ichizo Umehara Date: Thu, 11 Dec 2025 18:27:51 -0500 Subject: [PATCH 02/32] Fix imported types in chai.ts --- packages/vitest/src/integrations/snapshot/chai.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index aea6241ad441..3f2a0fcacce2 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -1,4 +1,4 @@ -import type { Assertion, ChaiPlugin } from '@vitest/expect' +import type { ChaiPlugin } from '@vitest/expect' import type { Test } from '@vitest/runner' import { equals, iterableEquality, subsetEquality } from '@vitest/expect' import { getNames } from '@vitest/runner/utils' @@ -63,7 +63,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { for (const key of ['matchSnapshot', 'toMatchSnapshot']) { utils.addMethod(chai.Assertion.prototype, key, wrapAssertion(utils, key, function ( - this: Chai.AssertionStatic & Assertion, + this: Chai.AssertionStatic & Chai.Assertion, properties?: object, message?: string, ) { @@ -93,7 +93,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { utils.addMethod( chai.Assertion.prototype, 'toMatchFileSnapshot', - wrapAssertion(utils, 'toMatchFileSnapshot', function (this: Assertion, file: string, message?: string) { + wrapAssertion(utils, 'toMatchFileSnapshot', function (this: Chai.AssertionStatic & Chai.Assertion, file: string, message?: string) { utils.flag(this, '_name', 'toMatchFileSnapshot') const isNot = utils.flag(this, 'negate') if (isNot) { @@ -124,7 +124,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { return recordAsyncExpect( test, promise, - createAssertionMessage(utils, this, true), + createAssertionMessage(utils, this as any, true), error, ) }), @@ -182,7 +182,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { utils.addMethod( chai.Assertion.prototype, 'toThrowErrorMatchingSnapshot', - wrapAssertion(utils, 'toThrowErrorMatchingSnapshot', function (this: Chai.AssertionStatic & Assertion, properties?: object, message?: string) { + wrapAssertion(utils, 'toThrowErrorMatchingSnapshot', function (this: Chai.AssertionStatic & Chai.Assertion, properties?: object, message?: string) { utils.flag(this, '_name', 'toThrowErrorMatchingSnapshot') const isNot = utils.flag(this, 'negate') if (isNot) { From 135dbc1fdd18d887fd2ddfa9b7d07f901fa15f12 Mon Sep 17 00:00:00 2001 From: Ichizo Umehara Date: Mon, 15 Dec 2025 23:28:45 -0500 Subject: [PATCH 03/32] Move expect.soft Snapshot tests to snapshot directory to test across create/update/read-only scenarios --- .../__snapshots__/snapshot-soft.test.ts.snap | 25 ------ test/core/test/snapshot-soft.test.ts | 79 ------------------- .../__snapshots__/soft-file.test.ts.snap | 13 +++ test/snapshots/test-update/soft-file.test.ts | 78 ++++++++++++++++++ .../snapshots/test-update/soft-inline.test.ts | 27 +++++++ .../__snapshots__/test-update.test.ts.snap | 31 ++++++++ .../__snapshots__/soft-file.test.ts.snap | 13 +++ .../fixtures/test-update/soft-file.test.ts | 78 ++++++++++++++++++ .../fixtures/test-update/soft-inline.test.ts | 27 +++++++ 9 files changed, 267 insertions(+), 104 deletions(-) delete mode 100644 test/core/test/__snapshots__/snapshot-soft.test.ts.snap delete mode 100644 test/core/test/snapshot-soft.test.ts create mode 100644 test/snapshots/test-update/__snapshots__/soft-file.test.ts.snap create mode 100644 test/snapshots/test-update/soft-file.test.ts create mode 100644 test/snapshots/test-update/soft-inline.test.ts create mode 100644 test/snapshots/test/fixtures/test-update/__snapshots__/soft-file.test.ts.snap create mode 100644 test/snapshots/test/fixtures/test-update/soft-file.test.ts create mode 100644 test/snapshots/test/fixtures/test-update/soft-inline.test.ts diff --git a/test/core/test/__snapshots__/snapshot-soft.test.ts.snap b/test/core/test/__snapshots__/snapshot-soft.test.ts.snap deleted file mode 100644 index b64f7f849614..000000000000 --- a/test/core/test/__snapshots__/snapshot-soft.test.ts.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`expect.soft tracks failures for matchSnapshot 1`] = ` -{ - "time": "2000-01-01T00:00:00.000Z", -} -`; - -exports[`expect.soft tracks failures for matchSnapshot 2`] = ` -{ - "uuid": "00000000-0000-0000-0000-000000000000", -} -`; - -exports[`expect.soft tracks failures for toMatchSnapshot 1`] = ` -{ - "time": "2000-01-01T00:00:00.000Z", -} -`; - -exports[`expect.soft tracks failures for toMatchSnapshot 2`] = ` -{ - "uuid": "00000000-0000-0000-0000-000000000000", -} -`; diff --git a/test/core/test/snapshot-soft.test.ts b/test/core/test/snapshot-soft.test.ts deleted file mode 100644 index fcbba3b9526f..000000000000 --- a/test/core/test/snapshot-soft.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { getCurrentTest } from '@vitest/runner' -import { expect, test } from 'vitest' - -test('expect.soft tracks failures for matchSnapshot', () => { - expect.soft({ time: new Date() }).matchSnapshot() - expect.soft({ uuid: crypto.randomUUID() }).matchSnapshot() - - expect(getCurrentTest()?.result?.errors).toHaveLength(2) - resetFailingTestState() - resetUnmatchedSnapshotFailures() -}) - -test('expect.soft tracks failures for toMatchSnapshot', async () => { - expect.soft({ time: new Date() }).toMatchSnapshot() - expect.soft({ uuid: crypto.randomUUID() }).toMatchSnapshot() - - expect(getCurrentTest()?.result?.errors).toHaveLength(2) - resetFailingTestState() - resetUnmatchedSnapshotFailures() -}) - -test('expect.soft tracks failures for toMatchFileSnapshot', async () => { - await expect.soft({ time: new Date() }).toMatchFileSnapshot('./__snapshots__/custom/custom_snapshot1') - await expect.soft({ uuid: crypto.randomUUID() }).toMatchFileSnapshot('./__snapshots__/custom/custom_snapshot2') - - expect(getCurrentTest()?.result?.errors).toHaveLength(2) - resetFailingTestState() - resetUnmatchedSnapshotFailures() -}) - -test('expect.soft tracks failures for toThrowErrorMatchingSnapshot', () => { - expect.soft({ time: '2011-11-11' }).toThrowErrorMatchingSnapshot() - expect.soft({ uuid: 'eb10615a-e798-4630-8209-4ab082345e78' }).toThrowErrorMatchingSnapshot() - - expect(getCurrentTest()?.result?.errors).toHaveLength(2) - resetFailingTestState() - resetUnmatchedSnapshotFailures() -}) - -test('expect.soft fails if run with toMatchInlineSnapshot', () => { - let exceptionThrown = false - try { - expect.soft(1).toMatchInlineSnapshot(`1`) - } - catch (error: any) { - expect(error?.message).toContain('toMatchInlineSnapshot cannot be used with "soft"') - exceptionThrown = true - } - - expect(exceptionThrown).toBe(true) - resetUnmatchedSnapshotFailures() -}) - -test('expect.soft fails if run with toThrowErrorMatchingInlineSnapshot', () => { - let exceptionThrown = false - try { - expect.soft(() => { - throw new Error('1') - }).toThrowErrorMatchingInlineSnapshot(`[Error: 2]`) - } - catch (error: any) { - expect(error?.message).toContain('toThrowErrorMatchingInlineSnapshot cannot be used with "soft"') - exceptionThrown = true - } - - expect(exceptionThrown).toBe(true) - resetUnmatchedSnapshotFailures() -}) - -function resetFailingTestState() { - getCurrentTest()!.result!.state = 'run' -} - -function resetUnmatchedSnapshotFailures() { - const snapshotState = expect.getState().snapshotState - if (snapshotState?.unmatched) { - snapshotState.unmatched = 0 - } -} diff --git a/test/snapshots/test-update/__snapshots__/soft-file.test.ts.snap b/test/snapshots/test-update/__snapshots__/soft-file.test.ts.snap new file mode 100644 index 000000000000..24d2f60ee474 --- /dev/null +++ b/test/snapshots/test-update/__snapshots__/soft-file.test.ts.snap @@ -0,0 +1,13 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`expect.soft tracks errors across successes for toMatchSnapshot 1`] = `"I have a matching snapshot"`; + +exports[`expect.soft tracks errors across successes for toMatchSnapshot 3`] = `"I also have a matching snapshot"`; + +exports[`expect.soft tracks errors across successes for toMatchSnapshot 5`] = `"Yet another matching snapshot"`; + +exports[`expect.soft tracks errors for matchSnapshot 2`] = `"I also have a matching snapshot"`; + +exports[`expect.soft tracks errors for toMatchFileSnapshot 2`] = `"Another matching snapshot"`; + +exports[`expect.soft tracks errors for toThrowErrorMatchingSnapshot 1`] = `"Yet another matching snapshot"`; diff --git a/test/snapshots/test-update/soft-file.test.ts b/test/snapshots/test-update/soft-file.test.ts new file mode 100644 index 000000000000..a474b07c1e0f --- /dev/null +++ b/test/snapshots/test-update/soft-file.test.ts @@ -0,0 +1,78 @@ +import type { NewPlugin } from '@vitest/pretty-format' +import { getCurrentTest } from '@vitest/runner' + +const AlwaysFailingSnapshotSerializer: NewPlugin = { + test: obj => obj && obj.forceFail === true, + serialize: () => { + throw new Error('Intentional snapshot serialization failure for testing') + }, +} +expect.addSnapshotSerializer(AlwaysFailingSnapshotSerializer) + +test('expect.soft tracks an error for toMatchSnapshot', () => { + expect.soft({ forceFail: true }).toMatchSnapshot() + + expect(getCurrentTest()?.result?.errors).toHaveLength(1) + expect(getCurrentTest()?.result?.state).toBe('fail') + passTestIfInFailedStateWithErrors() +}) + +test('expect.soft tracks multiple errors for toMatchSnapshot', () => { + expect.soft({ forceFail: true }).toMatchSnapshot() + expect.soft({ forceFail: true }).toMatchSnapshot() + + expect(getCurrentTest()?.result?.errors).toHaveLength(2) + expect(getCurrentTest()?.result?.state).toBe('fail') + passTestIfInFailedStateWithErrors() +}) + +test('expect.soft tracks errors across successes for toMatchSnapshot', () => { + expect.soft('I have a matching snapshot').toMatchSnapshot(); + expect.soft({ forceFail: true }).toMatchSnapshot() + expect.soft('I also have a matching snapshot').toMatchSnapshot(); + expect.soft({ forceFail: true }).toMatchSnapshot() + expect.soft('Yet another matching snapshot').toMatchSnapshot(); + + expect(getCurrentTest()?.result?.errors).toHaveLength(2) + expect(getCurrentTest()?.result?.state).toBe('fail') + passTestIfInFailedStateWithErrors() +}) + +test('expect.soft tracks errors for matchSnapshot', () => { + expect.soft({ forceFail: true }).matchSnapshot() + expect.soft('I also have a matching snapshot').toMatchSnapshot(); + expect.soft({ forceFail: true }).matchSnapshot() + + expect(getCurrentTest()?.result?.errors).toHaveLength(2) + expect(getCurrentTest()?.result?.state).toBe('fail') + passTestIfInFailedStateWithErrors() +}) + +test('expect.soft tracks errors for toMatchFileSnapshot', async() => { + await expect.soft({ forceFail: true }).toMatchFileSnapshot('fake-path/to-non-existent/file') + expect.soft('Another matching snapshot').toMatchSnapshot(); + await expect.soft({ forceFail: true }).toMatchFileSnapshot('fake-path/to-non-existent/file') + + expect(getCurrentTest()?.result?.errors).toHaveLength(2) + expect(getCurrentTest()?.result?.state).toBe('fail') + passTestIfInFailedStateWithErrors() +}) + +test('expect.soft tracks errors for toThrowErrorMatchingSnapshot', async() => { + expect.soft({ forceFail: true }).toThrowErrorMatchingSnapshot() + expect.soft('Yet another matching snapshot').toMatchSnapshot(); + expect.soft({ forceFail: true }).toThrowErrorMatchingSnapshot() + + expect(getCurrentTest()?.result?.errors).toHaveLength(2) + expect(getCurrentTest()?.result?.state).toBe('fail') + passTestIfInFailedStateWithErrors() +}) + +function passTestIfInFailedStateWithErrors() { + const result = getCurrentTest()!.result! + + if (result.errors && result.state === 'fail') { + result.state = 'pass' + } +} + diff --git a/test/snapshots/test-update/soft-inline.test.ts b/test/snapshots/test-update/soft-inline.test.ts new file mode 100644 index 000000000000..cdcfa44d7a91 --- /dev/null +++ b/test/snapshots/test-update/soft-inline.test.ts @@ -0,0 +1,27 @@ +test('expect.soft fails if run with toMatchInlineSnapshot', () => { + let exceptionThrown = false + try { + expect.soft(1).toMatchInlineSnapshot(`1`) + } + catch (error: any) { + expect(error?.message).toContain('toMatchInlineSnapshot cannot be used with "soft"') + exceptionThrown = true + } + + expect(exceptionThrown).toBe(true) +}) + +test('expect.soft fails if run with toThrowErrorMatchingInlineSnapshot', () => { + let exceptionThrown = false + try { + expect.soft(() => { + throw new Error('1') + }).toThrowErrorMatchingInlineSnapshot(`[Error: 2]`) + } + catch (error: any) { + expect(error?.message).toContain('toThrowErrorMatchingInlineSnapshot cannot be used with "soft"') + exceptionThrown = true + } + + expect(exceptionThrown).toBe(true) +}) diff --git a/test/snapshots/test/__snapshots__/test-update.test.ts.snap b/test/snapshots/test/__snapshots__/test-update.test.ts.snap index 3321ee7ef68e..88b9a1f8485d 100644 --- a/test/snapshots/test/__snapshots__/test-update.test.ts.snap +++ b/test/snapshots/test/__snapshots__/test-update.test.ts.snap @@ -125,3 +125,34 @@ test('same title', () => { }) " `; + +exports[`soft-inline.test.ts 1`] = ` +"test('expect.soft fails if run with toMatchInlineSnapshot', () => { + let exceptionThrown = false + try { + expect.soft(1).toMatchInlineSnapshot(\`1\`) + } + catch (error: any) { + expect(error?.message).toContain('toMatchInlineSnapshot cannot be used with "soft"') + exceptionThrown = true + } + + expect(exceptionThrown).toBe(true) +}) + +test('expect.soft fails if run with toThrowErrorMatchingInlineSnapshot', () => { + let exceptionThrown = false + try { + expect.soft(() => { + throw new Error('1') + }).toThrowErrorMatchingInlineSnapshot(\`[Error: 2]\`) + } + catch (error: any) { + expect(error?.message).toContain('toThrowErrorMatchingInlineSnapshot cannot be used with "soft"') + exceptionThrown = true + } + + expect(exceptionThrown).toBe(true) +}) +" +`; diff --git a/test/snapshots/test/fixtures/test-update/__snapshots__/soft-file.test.ts.snap b/test/snapshots/test/fixtures/test-update/__snapshots__/soft-file.test.ts.snap new file mode 100644 index 000000000000..24d2f60ee474 --- /dev/null +++ b/test/snapshots/test/fixtures/test-update/__snapshots__/soft-file.test.ts.snap @@ -0,0 +1,13 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`expect.soft tracks errors across successes for toMatchSnapshot 1`] = `"I have a matching snapshot"`; + +exports[`expect.soft tracks errors across successes for toMatchSnapshot 3`] = `"I also have a matching snapshot"`; + +exports[`expect.soft tracks errors across successes for toMatchSnapshot 5`] = `"Yet another matching snapshot"`; + +exports[`expect.soft tracks errors for matchSnapshot 2`] = `"I also have a matching snapshot"`; + +exports[`expect.soft tracks errors for toMatchFileSnapshot 2`] = `"Another matching snapshot"`; + +exports[`expect.soft tracks errors for toThrowErrorMatchingSnapshot 1`] = `"Yet another matching snapshot"`; diff --git a/test/snapshots/test/fixtures/test-update/soft-file.test.ts b/test/snapshots/test/fixtures/test-update/soft-file.test.ts new file mode 100644 index 000000000000..a474b07c1e0f --- /dev/null +++ b/test/snapshots/test/fixtures/test-update/soft-file.test.ts @@ -0,0 +1,78 @@ +import type { NewPlugin } from '@vitest/pretty-format' +import { getCurrentTest } from '@vitest/runner' + +const AlwaysFailingSnapshotSerializer: NewPlugin = { + test: obj => obj && obj.forceFail === true, + serialize: () => { + throw new Error('Intentional snapshot serialization failure for testing') + }, +} +expect.addSnapshotSerializer(AlwaysFailingSnapshotSerializer) + +test('expect.soft tracks an error for toMatchSnapshot', () => { + expect.soft({ forceFail: true }).toMatchSnapshot() + + expect(getCurrentTest()?.result?.errors).toHaveLength(1) + expect(getCurrentTest()?.result?.state).toBe('fail') + passTestIfInFailedStateWithErrors() +}) + +test('expect.soft tracks multiple errors for toMatchSnapshot', () => { + expect.soft({ forceFail: true }).toMatchSnapshot() + expect.soft({ forceFail: true }).toMatchSnapshot() + + expect(getCurrentTest()?.result?.errors).toHaveLength(2) + expect(getCurrentTest()?.result?.state).toBe('fail') + passTestIfInFailedStateWithErrors() +}) + +test('expect.soft tracks errors across successes for toMatchSnapshot', () => { + expect.soft('I have a matching snapshot').toMatchSnapshot(); + expect.soft({ forceFail: true }).toMatchSnapshot() + expect.soft('I also have a matching snapshot').toMatchSnapshot(); + expect.soft({ forceFail: true }).toMatchSnapshot() + expect.soft('Yet another matching snapshot').toMatchSnapshot(); + + expect(getCurrentTest()?.result?.errors).toHaveLength(2) + expect(getCurrentTest()?.result?.state).toBe('fail') + passTestIfInFailedStateWithErrors() +}) + +test('expect.soft tracks errors for matchSnapshot', () => { + expect.soft({ forceFail: true }).matchSnapshot() + expect.soft('I also have a matching snapshot').toMatchSnapshot(); + expect.soft({ forceFail: true }).matchSnapshot() + + expect(getCurrentTest()?.result?.errors).toHaveLength(2) + expect(getCurrentTest()?.result?.state).toBe('fail') + passTestIfInFailedStateWithErrors() +}) + +test('expect.soft tracks errors for toMatchFileSnapshot', async() => { + await expect.soft({ forceFail: true }).toMatchFileSnapshot('fake-path/to-non-existent/file') + expect.soft('Another matching snapshot').toMatchSnapshot(); + await expect.soft({ forceFail: true }).toMatchFileSnapshot('fake-path/to-non-existent/file') + + expect(getCurrentTest()?.result?.errors).toHaveLength(2) + expect(getCurrentTest()?.result?.state).toBe('fail') + passTestIfInFailedStateWithErrors() +}) + +test('expect.soft tracks errors for toThrowErrorMatchingSnapshot', async() => { + expect.soft({ forceFail: true }).toThrowErrorMatchingSnapshot() + expect.soft('Yet another matching snapshot').toMatchSnapshot(); + expect.soft({ forceFail: true }).toThrowErrorMatchingSnapshot() + + expect(getCurrentTest()?.result?.errors).toHaveLength(2) + expect(getCurrentTest()?.result?.state).toBe('fail') + passTestIfInFailedStateWithErrors() +}) + +function passTestIfInFailedStateWithErrors() { + const result = getCurrentTest()!.result! + + if (result.errors && result.state === 'fail') { + result.state = 'pass' + } +} + diff --git a/test/snapshots/test/fixtures/test-update/soft-inline.test.ts b/test/snapshots/test/fixtures/test-update/soft-inline.test.ts new file mode 100644 index 000000000000..cdcfa44d7a91 --- /dev/null +++ b/test/snapshots/test/fixtures/test-update/soft-inline.test.ts @@ -0,0 +1,27 @@ +test('expect.soft fails if run with toMatchInlineSnapshot', () => { + let exceptionThrown = false + try { + expect.soft(1).toMatchInlineSnapshot(`1`) + } + catch (error: any) { + expect(error?.message).toContain('toMatchInlineSnapshot cannot be used with "soft"') + exceptionThrown = true + } + + expect(exceptionThrown).toBe(true) +}) + +test('expect.soft fails if run with toThrowErrorMatchingInlineSnapshot', () => { + let exceptionThrown = false + try { + expect.soft(() => { + throw new Error('1') + }).toThrowErrorMatchingInlineSnapshot(`[Error: 2]`) + } + catch (error: any) { + expect(error?.message).toContain('toThrowErrorMatchingInlineSnapshot cannot be used with "soft"') + exceptionThrown = true + } + + expect(exceptionThrown).toBe(true) +}) From 84e80f7b96fd92a8006a7b1611be32a6135dc207 Mon Sep 17 00:00:00 2001 From: Ichizo Umehara Date: Mon, 26 Jan 2026 23:58:08 -0500 Subject: [PATCH 04/32] Fix linting errors --- test/snapshots/test-update/soft-file.test.ts | 32 ++++++++--------- .../snapshots/test-update/soft-inline.test.ts | 2 ++ .../fixtures/test-update/soft-file.test.ts | 34 +++++++++---------- .../fixtures/test-update/soft-inline.test.ts | 2 ++ 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/test/snapshots/test-update/soft-file.test.ts b/test/snapshots/test-update/soft-file.test.ts index a474b07c1e0f..c36e9c5bcc95 100644 --- a/test/snapshots/test-update/soft-file.test.ts +++ b/test/snapshots/test-update/soft-file.test.ts @@ -1,5 +1,6 @@ import type { NewPlugin } from '@vitest/pretty-format' import { getCurrentTest } from '@vitest/runner' +import { expect, test } from 'vitest' const AlwaysFailingSnapshotSerializer: NewPlugin = { test: obj => obj && obj.forceFail === true, @@ -11,7 +12,7 @@ expect.addSnapshotSerializer(AlwaysFailingSnapshotSerializer) test('expect.soft tracks an error for toMatchSnapshot', () => { expect.soft({ forceFail: true }).toMatchSnapshot() - + expect(getCurrentTest()?.result?.errors).toHaveLength(1) expect(getCurrentTest()?.result?.state).toBe('fail') passTestIfInFailedStateWithErrors() @@ -20,19 +21,19 @@ test('expect.soft tracks an error for toMatchSnapshot', () => { test('expect.soft tracks multiple errors for toMatchSnapshot', () => { expect.soft({ forceFail: true }).toMatchSnapshot() expect.soft({ forceFail: true }).toMatchSnapshot() - + expect(getCurrentTest()?.result?.errors).toHaveLength(2) expect(getCurrentTest()?.result?.state).toBe('fail') passTestIfInFailedStateWithErrors() }) test('expect.soft tracks errors across successes for toMatchSnapshot', () => { - expect.soft('I have a matching snapshot').toMatchSnapshot(); + expect.soft('I have a matching snapshot').toMatchSnapshot() expect.soft({ forceFail: true }).toMatchSnapshot() - expect.soft('I also have a matching snapshot').toMatchSnapshot(); + expect.soft('I also have a matching snapshot').toMatchSnapshot() expect.soft({ forceFail: true }).toMatchSnapshot() - expect.soft('Yet another matching snapshot').toMatchSnapshot(); - + expect.soft('Yet another matching snapshot').toMatchSnapshot() + expect(getCurrentTest()?.result?.errors).toHaveLength(2) expect(getCurrentTest()?.result?.state).toBe('fail') passTestIfInFailedStateWithErrors() @@ -40,27 +41,27 @@ test('expect.soft tracks errors across successes for toMatchSnapshot', () => { test('expect.soft tracks errors for matchSnapshot', () => { expect.soft({ forceFail: true }).matchSnapshot() - expect.soft('I also have a matching snapshot').toMatchSnapshot(); + expect.soft('I also have a matching snapshot').toMatchSnapshot() expect.soft({ forceFail: true }).matchSnapshot() - + expect(getCurrentTest()?.result?.errors).toHaveLength(2) expect(getCurrentTest()?.result?.state).toBe('fail') passTestIfInFailedStateWithErrors() }) -test('expect.soft tracks errors for toMatchFileSnapshot', async() => { +test('expect.soft tracks errors for toMatchFileSnapshot', async () => { await expect.soft({ forceFail: true }).toMatchFileSnapshot('fake-path/to-non-existent/file') - expect.soft('Another matching snapshot').toMatchSnapshot(); + expect.soft('Another matching snapshot').toMatchSnapshot() await expect.soft({ forceFail: true }).toMatchFileSnapshot('fake-path/to-non-existent/file') - + expect(getCurrentTest()?.result?.errors).toHaveLength(2) expect(getCurrentTest()?.result?.state).toBe('fail') passTestIfInFailedStateWithErrors() }) -test('expect.soft tracks errors for toThrowErrorMatchingSnapshot', async() => { +test('expect.soft tracks errors for toThrowErrorMatchingSnapshot', async () => { expect.soft({ forceFail: true }).toThrowErrorMatchingSnapshot() - expect.soft('Yet another matching snapshot').toMatchSnapshot(); + expect.soft('Yet another matching snapshot').toMatchSnapshot() expect.soft({ forceFail: true }).toThrowErrorMatchingSnapshot() expect(getCurrentTest()?.result?.errors).toHaveLength(2) @@ -70,9 +71,8 @@ test('expect.soft tracks errors for toThrowErrorMatchingSnapshot', async() => { function passTestIfInFailedStateWithErrors() { const result = getCurrentTest()!.result! - + if (result.errors && result.state === 'fail') { - result.state = 'pass' + result.state = 'pass' } } - diff --git a/test/snapshots/test-update/soft-inline.test.ts b/test/snapshots/test-update/soft-inline.test.ts index cdcfa44d7a91..00686358a0c4 100644 --- a/test/snapshots/test-update/soft-inline.test.ts +++ b/test/snapshots/test-update/soft-inline.test.ts @@ -1,3 +1,5 @@ +import { expect, test } from 'vitest' + test('expect.soft fails if run with toMatchInlineSnapshot', () => { let exceptionThrown = false try { diff --git a/test/snapshots/test/fixtures/test-update/soft-file.test.ts b/test/snapshots/test/fixtures/test-update/soft-file.test.ts index a474b07c1e0f..56adcecbbca9 100644 --- a/test/snapshots/test/fixtures/test-update/soft-file.test.ts +++ b/test/snapshots/test/fixtures/test-update/soft-file.test.ts @@ -1,3 +1,4 @@ +import { expect, test } from 'vitest' import type { NewPlugin } from '@vitest/pretty-format' import { getCurrentTest } from '@vitest/runner' @@ -11,7 +12,7 @@ expect.addSnapshotSerializer(AlwaysFailingSnapshotSerializer) test('expect.soft tracks an error for toMatchSnapshot', () => { expect.soft({ forceFail: true }).toMatchSnapshot() - + expect(getCurrentTest()?.result?.errors).toHaveLength(1) expect(getCurrentTest()?.result?.state).toBe('fail') passTestIfInFailedStateWithErrors() @@ -20,19 +21,19 @@ test('expect.soft tracks an error for toMatchSnapshot', () => { test('expect.soft tracks multiple errors for toMatchSnapshot', () => { expect.soft({ forceFail: true }).toMatchSnapshot() expect.soft({ forceFail: true }).toMatchSnapshot() - + expect(getCurrentTest()?.result?.errors).toHaveLength(2) expect(getCurrentTest()?.result?.state).toBe('fail') passTestIfInFailedStateWithErrors() }) test('expect.soft tracks errors across successes for toMatchSnapshot', () => { - expect.soft('I have a matching snapshot').toMatchSnapshot(); + expect.soft('I have a matching snapshot').toMatchSnapshot() expect.soft({ forceFail: true }).toMatchSnapshot() - expect.soft('I also have a matching snapshot').toMatchSnapshot(); + expect.soft('I also have a matching snapshot').toMatchSnapshot() expect.soft({ forceFail: true }).toMatchSnapshot() - expect.soft('Yet another matching snapshot').toMatchSnapshot(); - + expect.soft('Yet another matching snapshot').toMatchSnapshot() + expect(getCurrentTest()?.result?.errors).toHaveLength(2) expect(getCurrentTest()?.result?.state).toBe('fail') passTestIfInFailedStateWithErrors() @@ -40,27 +41,27 @@ test('expect.soft tracks errors across successes for toMatchSnapshot', () => { test('expect.soft tracks errors for matchSnapshot', () => { expect.soft({ forceFail: true }).matchSnapshot() - expect.soft('I also have a matching snapshot').toMatchSnapshot(); + expect.soft('I also have a matching snapshot').toMatchSnapshot() expect.soft({ forceFail: true }).matchSnapshot() - + expect(getCurrentTest()?.result?.errors).toHaveLength(2) expect(getCurrentTest()?.result?.state).toBe('fail') passTestIfInFailedStateWithErrors() }) -test('expect.soft tracks errors for toMatchFileSnapshot', async() => { +test('expect.soft tracks errors for toMatchFileSnapshot', async () => { await expect.soft({ forceFail: true }).toMatchFileSnapshot('fake-path/to-non-existent/file') - expect.soft('Another matching snapshot').toMatchSnapshot(); + expect.soft('Another matching snapshot').toMatchSnapshot() await expect.soft({ forceFail: true }).toMatchFileSnapshot('fake-path/to-non-existent/file') - + expect(getCurrentTest()?.result?.errors).toHaveLength(2) expect(getCurrentTest()?.result?.state).toBe('fail') passTestIfInFailedStateWithErrors() }) -test('expect.soft tracks errors for toThrowErrorMatchingSnapshot', async() => { +test('expect.soft tracks errors for toThrowErrorMatchingSnapshot', async () => { expect.soft({ forceFail: true }).toThrowErrorMatchingSnapshot() - expect.soft('Yet another matching snapshot').toMatchSnapshot(); + expect.soft('Yet another matching snapshot').toMatchSnapshot() expect.soft({ forceFail: true }).toThrowErrorMatchingSnapshot() expect(getCurrentTest()?.result?.errors).toHaveLength(2) @@ -70,9 +71,8 @@ test('expect.soft tracks errors for toThrowErrorMatchingSnapshot', async() => { function passTestIfInFailedStateWithErrors() { const result = getCurrentTest()!.result! - + if (result.errors && result.state === 'fail') { - result.state = 'pass' + result.state = 'pass' } -} - +} \ No newline at end of file diff --git a/test/snapshots/test/fixtures/test-update/soft-inline.test.ts b/test/snapshots/test/fixtures/test-update/soft-inline.test.ts index cdcfa44d7a91..00686358a0c4 100644 --- a/test/snapshots/test/fixtures/test-update/soft-inline.test.ts +++ b/test/snapshots/test/fixtures/test-update/soft-inline.test.ts @@ -1,3 +1,5 @@ +import { expect, test } from 'vitest' + test('expect.soft fails if run with toMatchInlineSnapshot', () => { let exceptionThrown = false try { From ddb7747db4fc546a0ec38d2223035209f638c450 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 3 Feb 2026 18:48:25 +0900 Subject: [PATCH 05/32] test: add e2e --- test/snapshots/test/fixtures/soft/.gitignore | 1 + .../test/fixtures/soft/basic.test.ts | 16 ++ test/snapshots/test/soft-inline.test.ts | 7 + test/snapshots/test/soft.test.ts | 197 ++++++++++++++++++ 4 files changed, 221 insertions(+) create mode 100644 test/snapshots/test/fixtures/soft/.gitignore create mode 100644 test/snapshots/test/fixtures/soft/basic.test.ts create mode 100644 test/snapshots/test/soft-inline.test.ts create mode 100644 test/snapshots/test/soft.test.ts diff --git a/test/snapshots/test/fixtures/soft/.gitignore b/test/snapshots/test/fixtures/soft/.gitignore new file mode 100644 index 000000000000..b05c2dfa7007 --- /dev/null +++ b/test/snapshots/test/fixtures/soft/.gitignore @@ -0,0 +1 @@ +__snapshots__ diff --git a/test/snapshots/test/fixtures/soft/basic.test.ts b/test/snapshots/test/fixtures/soft/basic.test.ts new file mode 100644 index 000000000000..3ddc4c68db40 --- /dev/null +++ b/test/snapshots/test/fixtures/soft/basic.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from 'vitest' + +test('toMatchSnapshot', () => { + expect.soft('--snap-1--').toMatchSnapshot() + expect.soft('--snap-2--').toMatchSnapshot() +}) + +test('toMatchFileSnapshot', async () => { + await expect.soft('--file-1--').toMatchFileSnapshot('./__snapshots__/custom1.txt') + await expect.soft('--file-2--').toMatchFileSnapshot('./__snapshots__/custom2.txt') +}) + +test('toThrowErrorMatchingSnapshot', () => { + expect.soft(() => { throw new Error('--error-1--') }).toThrowErrorMatchingSnapshot() + expect.soft(() => { throw new Error('--error-2--') }).toThrowErrorMatchingSnapshot() +}) diff --git a/test/snapshots/test/soft-inline.test.ts b/test/snapshots/test/soft-inline.test.ts new file mode 100644 index 000000000000..29b1b6ae675d --- /dev/null +++ b/test/snapshots/test/soft-inline.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from 'vitest' + +test('not supported yet', () => { + expect(() => expect.soft('test').toMatchInlineSnapshot()).toThrowErrorMatchingInlineSnapshot( + `[Error: toMatchInlineSnapshot cannot be used with "soft"]`, + ) +}) diff --git a/test/snapshots/test/soft.test.ts b/test/snapshots/test/soft.test.ts new file mode 100644 index 000000000000..b896e7d1bdf9 --- /dev/null +++ b/test/snapshots/test/soft.test.ts @@ -0,0 +1,197 @@ +import fs, { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { expect, test } from 'vitest' +import { editFile, runVitest } from '../../test-utils' + +test('soft snapshot failures in read-only mode', async () => { + const root = join(import.meta.dirname, 'fixtures/soft') + const testFile = join(root, 'basic.test.ts') + const snapshotFile = join(root, '__snapshots__/basic.test.ts.snap') + const customFile1 = join(root, '__snapshots__/custom1.txt') + const customFile2 = join(root, '__snapshots__/custom2.txt') + + // reset and create snapshots from scratch + fs.rmSync(join(root, '__snapshots__'), { recursive: true, force: true }) + let result = await runVitest({ root, update: 'new' }) + expect(result.stderr).toMatchInlineSnapshot(`""`) + expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + + exports[\`toMatchSnapshot 1\`] = \`"--snap-1--"\`; + + exports[\`toMatchSnapshot 2\`] = \`"--snap-2--"\`; + + exports[\`toThrowErrorMatchingSnapshot 1\`] = \`[Error: --error-1--]\`; + + exports[\`toThrowErrorMatchingSnapshot 2\`] = \`[Error: --error-2--]\`; + " + `) + expect(readFileSync(customFile1, 'utf-8')).toMatchInlineSnapshot(`"--file-1--"`) + expect(readFileSync(customFile2, 'utf-8')).toMatchInlineSnapshot(`"--file-2--"`) + expect(result.errorTree()).toMatchInlineSnapshot(` + Object { + "basic.test.ts": Object { + "toMatchFileSnapshot": "passed", + "toMatchSnapshot": "passed", + "toThrowErrorMatchingSnapshot": "passed", + }, + } + `) + + // edit tests to introduce snapshot errors + editFile(testFile, s => s + .replace(`--snap-1--`, `--snap-1-edit--`) + .replace(`--snap-2--`, `--snap-2-edit--`) + .replace(`--file-1--`, `--file-1-edit--`) + .replace(`--file-2--`, `--file-2-edit--`) + .replace(`--error-1--`, `--error-1-edit--`) + .replace(`--error-2--`, `--error-2-edit--`)) + result = await runVitest({ root, update: false }) + expect(result.stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL basic.test.ts > toMatchSnapshot + Error: Snapshot \`toMatchSnapshot 1\` mismatched + + Expected: ""--snap-1--"" + Received: ""--snap-1-edit--"" + + ❯ basic.test.ts:4:34 + 2| + 3| test('toMatchSnapshot', () => { + 4| expect.soft('--snap-1-edit--').toMatchSnapshot() + | ^ + 5| expect.soft('--snap-2-edit--').toMatchSnapshot() + 6| }) + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/6]⎯ + + FAIL basic.test.ts > toMatchSnapshot + Error: Snapshot \`toMatchSnapshot 2\` mismatched + + Expected: ""--snap-2--"" + Received: ""--snap-2-edit--"" + + ❯ basic.test.ts:5:34 + 3| test('toMatchSnapshot', () => { + 4| expect.soft('--snap-1-edit--').toMatchSnapshot() + 5| expect.soft('--snap-2-edit--').toMatchSnapshot() + | ^ + 6| }) + 7| + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/6]⎯ + + FAIL basic.test.ts > toMatchFileSnapshot + Error: Snapshot \`toMatchFileSnapshot 1\` mismatched + + Expected: "--file-1--" + Received: "--file-1-edit--" + + ❯ basic.test.ts:9:3 + 7| + 8| test('toMatchFileSnapshot', async () => { + 9| await expect.soft('--file-1-edit--').toMatchFileSnapshot('./__snapsh… + | ^ + 10| await expect.soft('--file-2-edit--').toMatchFileSnapshot('./__snapsh… + 11| }) + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/6]⎯ + + FAIL basic.test.ts > toMatchFileSnapshot + Error: Snapshot \`toMatchFileSnapshot 2\` mismatched + + Expected: "--file-2--" + Received: "--file-2-edit--" + + ❯ basic.test.ts:10:3 + 8| test('toMatchFileSnapshot', async () => { + 9| await expect.soft('--file-1-edit--').toMatchFileSnapshot('./__snapsh… + 10| await expect.soft('--file-2-edit--').toMatchFileSnapshot('./__snapsh… + | ^ + 11| }) + 12| + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/6]⎯ + + FAIL basic.test.ts > toThrowErrorMatchingSnapshot + Error: Snapshot \`toThrowErrorMatchingSnapshot 1\` mismatched + + Expected: "[Error: --error-1--]" + Received: "[Error: --error-1-edit--]" + + ❯ basic.test.ts:14:62 + 12| + 13| test('toThrowErrorMatchingSnapshot', () => { + 14| expect.soft(() => { throw new Error('--error-1-edit--') }).toThrowEr… + | ^ + 15| expect.soft(() => { throw new Error('--error-2-edit--') }).toThrowEr… + 16| }) + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/6]⎯ + + FAIL basic.test.ts > toThrowErrorMatchingSnapshot + Error: Snapshot \`toThrowErrorMatchingSnapshot 2\` mismatched + + Expected: "[Error: --error-2--]" + Received: "[Error: --error-2-edit--]" + + ❯ basic.test.ts:15:62 + 13| test('toThrowErrorMatchingSnapshot', () => { + 14| expect.soft(() => { throw new Error('--error-1-edit--') }).toThrowEr… + 15| expect.soft(() => { throw new Error('--error-2-edit--') }).toThrowEr… + | ^ + 16| }) + 17| + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/6]⎯ + + " + `) + expect(result.errorTree()).toMatchInlineSnapshot(` + Object { + "basic.test.ts": Object { + "toMatchFileSnapshot": Array [ + "Snapshot \`toMatchFileSnapshot 1\` mismatched", + "Snapshot \`toMatchFileSnapshot 2\` mismatched", + ], + "toMatchSnapshot": Array [ + "Snapshot \`toMatchSnapshot 1\` mismatched", + "Snapshot \`toMatchSnapshot 2\` mismatched", + ], + "toThrowErrorMatchingSnapshot": Array [ + "Snapshot \`toThrowErrorMatchingSnapshot 1\` mismatched", + "Snapshot \`toThrowErrorMatchingSnapshot 2\` mismatched", + ], + }, + } + `) + + // run with update + result = await runVitest({ root, update: 'all' }) + expect(result.stderr).toMatchInlineSnapshot(`""`) + expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + + exports[\`toMatchSnapshot 1\`] = \`"--snap-1-edit--"\`; + + exports[\`toMatchSnapshot 2\`] = \`"--snap-2-edit--"\`; + + exports[\`toThrowErrorMatchingSnapshot 1\`] = \`[Error: --error-1-edit--]\`; + + exports[\`toThrowErrorMatchingSnapshot 2\`] = \`[Error: --error-2-edit--]\`; + " + `) + expect(readFileSync(customFile1, 'utf-8')).toMatchInlineSnapshot(`"--file-1-edit--"`) + expect(readFileSync(customFile2, 'utf-8')).toMatchInlineSnapshot(`"--file-2-edit--"`) + expect(result.errorTree()).toMatchInlineSnapshot(` + Object { + "basic.test.ts": Object { + "toMatchFileSnapshot": "passed", + "toMatchSnapshot": "passed", + "toThrowErrorMatchingSnapshot": "passed", + }, + } + `) +}) From 654bb371cf1b208ce2133f1c6520c1425b3f2317 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 3 Feb 2026 18:52:16 +0900 Subject: [PATCH 06/32] test: remove artificial tests --- .../__snapshots__/custom/custom_snapshot1 | 3 - .../__snapshots__/custom/custom_snapshot2 | 3 - test/snapshots/README.md | 1 + .../__snapshots__/test-update.test.ts.snap | 158 ------------------ .../__snapshots__/soft-file.test.ts.snap | 13 -- .../fixtures/test-update/soft-file.test.ts | 78 --------- .../fixtures/test-update/soft-inline.test.ts | 29 ---- 7 files changed, 1 insertion(+), 284 deletions(-) delete mode 100644 test/core/test/__snapshots__/custom/custom_snapshot1 delete mode 100644 test/core/test/__snapshots__/custom/custom_snapshot2 create mode 100644 test/snapshots/README.md delete mode 100644 test/snapshots/test/__snapshots__/test-update.test.ts.snap delete mode 100644 test/snapshots/test/fixtures/test-update/__snapshots__/soft-file.test.ts.snap delete mode 100644 test/snapshots/test/fixtures/test-update/soft-file.test.ts delete mode 100644 test/snapshots/test/fixtures/test-update/soft-inline.test.ts diff --git a/test/core/test/__snapshots__/custom/custom_snapshot1 b/test/core/test/__snapshots__/custom/custom_snapshot1 deleted file mode 100644 index d88d04d0711a..000000000000 --- a/test/core/test/__snapshots__/custom/custom_snapshot1 +++ /dev/null @@ -1,3 +0,0 @@ -{ - "time": 2025-12-11T05:21:52.329Z, -} \ No newline at end of file diff --git a/test/core/test/__snapshots__/custom/custom_snapshot2 b/test/core/test/__snapshots__/custom/custom_snapshot2 deleted file mode 100644 index 7513d971361e..000000000000 --- a/test/core/test/__snapshots__/custom/custom_snapshot2 +++ /dev/null @@ -1,3 +0,0 @@ -{ - "uuid": "e406eb00-df24-4070-8742-0c3bc05de89d", -} \ No newline at end of file diff --git a/test/snapshots/README.md b/test/snapshots/README.md new file mode 100644 index 000000000000..2c327219eb16 --- /dev/null +++ b/test/snapshots/README.md @@ -0,0 +1 @@ +TODO: explain which test script does what. diff --git a/test/snapshots/test/__snapshots__/test-update.test.ts.snap b/test/snapshots/test/__snapshots__/test-update.test.ts.snap deleted file mode 100644 index 88b9a1f8485d..000000000000 --- a/test/snapshots/test/__snapshots__/test-update.test.ts.snap +++ /dev/null @@ -1,158 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`inline.test.js 1`] = ` -"import { describe, expect, test } from 'vitest' - -// when snapshots are generated Vitest reruns \`toMatchInlineSnapshot\` checks -// please, don't commit generated snapshots -describe('snapshots are generated in correct order', async () => { - test('first snapshot', () => { - expect({ foo: ['bar'] }).toMatchInlineSnapshot(\` - Object { - "foo": Array [ - "bar", - ], - } - \`) - }) - - test('second snapshot', () => { - expect({ foo: ['zed'] }).toMatchInlineSnapshot(\` - Object { - "foo": Array [ - "zed", - ], - } - \`) - }) -}) - -describe('snapshots with properties', () => { - test('without snapshot', () => { - expect({ foo: 'bar' }).toMatchInlineSnapshot({ foo: expect.any(String) }, \` - Object { - "foo": Any, - } - \`) - }) - - test('with snapshot', () => { - expect({ first: { second: { foo: 'bar' } } }).toMatchInlineSnapshot({ first: { second: { foo: expect.any(String) } } }, \` - Object { - "first": Object { - "second": Object { - "foo": Any, - }, - }, - } - \`) - }) - - test('mixed with and without snapshot', () => { - expect({ first: { second: { foo: 'bar' } } }).toMatchInlineSnapshot({ first: { second: { foo: expect.any(String) } } }, \` - Object { - "first": Object { - "second": Object { - "foo": Any, - }, - }, - } - \`) - - expect({ first: { second: { foo: 'zed' } } }).toMatchInlineSnapshot(\` - Object { - "first": Object { - "second": Object { - "foo": "zed", - }, - }, - } - \`) - }) -}) -" -`; - -exports[`inline-concurrent.test.js 1`] = ` -"import { it } from 'vitest' - -it.concurrent('1st', ({ expect }) => { - expect('hi1').toMatchInlineSnapshot(\`"hi1"\`) -}) - -it.concurrent('2nd', ({ expect }) => { - expect('hi2').toMatchInlineSnapshot(\`"hi2"\`) -}) - -it.concurrent('3rd', ({ expect }) => { - expect('hi3').toMatchInlineSnapshot(\`"hi3"\`) -}) -" -`; - -exports[`retry-inline.test.ts 1`] = ` -"import { expect, test } from 'vitest' - -test('inline repeats', { repeats: 1 }, () => { - expect('foo').toMatchInlineSnapshot(\`"foo"\`) -}) - -test('inline retry', { retry: 1 }, (ctx) => { - expect('foo').toMatchInlineSnapshot(\`"foo"\`) - if (ctx.task.result?.retryCount === 0) { - throw new Error('boom') - } -}) -" -`; - -exports[`same-title-inline.test.js 1`] = ` -"import { expect, test } from 'vitest' - -test('same title', () => { - expect('new').toMatchInlineSnapshot(\`"new"\`) - expect('new').toMatchInlineSnapshot(\`"new"\`) -}) - -test('same title', () => { - expect('a').toMatchInlineSnapshot(\`"a"\`) - expect('a').toMatchInlineSnapshot(\`"a"\`) -}) - -test('same title', () => { - expect('b').toMatchInlineSnapshot(\`"b"\`) - expect('b').toMatchInlineSnapshot(\`"b"\`) -}) -" -`; - -exports[`soft-inline.test.ts 1`] = ` -"test('expect.soft fails if run with toMatchInlineSnapshot', () => { - let exceptionThrown = false - try { - expect.soft(1).toMatchInlineSnapshot(\`1\`) - } - catch (error: any) { - expect(error?.message).toContain('toMatchInlineSnapshot cannot be used with "soft"') - exceptionThrown = true - } - - expect(exceptionThrown).toBe(true) -}) - -test('expect.soft fails if run with toThrowErrorMatchingInlineSnapshot', () => { - let exceptionThrown = false - try { - expect.soft(() => { - throw new Error('1') - }).toThrowErrorMatchingInlineSnapshot(\`[Error: 2]\`) - } - catch (error: any) { - expect(error?.message).toContain('toThrowErrorMatchingInlineSnapshot cannot be used with "soft"') - exceptionThrown = true - } - - expect(exceptionThrown).toBe(true) -}) -" -`; diff --git a/test/snapshots/test/fixtures/test-update/__snapshots__/soft-file.test.ts.snap b/test/snapshots/test/fixtures/test-update/__snapshots__/soft-file.test.ts.snap deleted file mode 100644 index 24d2f60ee474..000000000000 --- a/test/snapshots/test/fixtures/test-update/__snapshots__/soft-file.test.ts.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`expect.soft tracks errors across successes for toMatchSnapshot 1`] = `"I have a matching snapshot"`; - -exports[`expect.soft tracks errors across successes for toMatchSnapshot 3`] = `"I also have a matching snapshot"`; - -exports[`expect.soft tracks errors across successes for toMatchSnapshot 5`] = `"Yet another matching snapshot"`; - -exports[`expect.soft tracks errors for matchSnapshot 2`] = `"I also have a matching snapshot"`; - -exports[`expect.soft tracks errors for toMatchFileSnapshot 2`] = `"Another matching snapshot"`; - -exports[`expect.soft tracks errors for toThrowErrorMatchingSnapshot 1`] = `"Yet another matching snapshot"`; diff --git a/test/snapshots/test/fixtures/test-update/soft-file.test.ts b/test/snapshots/test/fixtures/test-update/soft-file.test.ts deleted file mode 100644 index 56adcecbbca9..000000000000 --- a/test/snapshots/test/fixtures/test-update/soft-file.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { expect, test } from 'vitest' -import type { NewPlugin } from '@vitest/pretty-format' -import { getCurrentTest } from '@vitest/runner' - -const AlwaysFailingSnapshotSerializer: NewPlugin = { - test: obj => obj && obj.forceFail === true, - serialize: () => { - throw new Error('Intentional snapshot serialization failure for testing') - }, -} -expect.addSnapshotSerializer(AlwaysFailingSnapshotSerializer) - -test('expect.soft tracks an error for toMatchSnapshot', () => { - expect.soft({ forceFail: true }).toMatchSnapshot() - - expect(getCurrentTest()?.result?.errors).toHaveLength(1) - expect(getCurrentTest()?.result?.state).toBe('fail') - passTestIfInFailedStateWithErrors() -}) - -test('expect.soft tracks multiple errors for toMatchSnapshot', () => { - expect.soft({ forceFail: true }).toMatchSnapshot() - expect.soft({ forceFail: true }).toMatchSnapshot() - - expect(getCurrentTest()?.result?.errors).toHaveLength(2) - expect(getCurrentTest()?.result?.state).toBe('fail') - passTestIfInFailedStateWithErrors() -}) - -test('expect.soft tracks errors across successes for toMatchSnapshot', () => { - expect.soft('I have a matching snapshot').toMatchSnapshot() - expect.soft({ forceFail: true }).toMatchSnapshot() - expect.soft('I also have a matching snapshot').toMatchSnapshot() - expect.soft({ forceFail: true }).toMatchSnapshot() - expect.soft('Yet another matching snapshot').toMatchSnapshot() - - expect(getCurrentTest()?.result?.errors).toHaveLength(2) - expect(getCurrentTest()?.result?.state).toBe('fail') - passTestIfInFailedStateWithErrors() -}) - -test('expect.soft tracks errors for matchSnapshot', () => { - expect.soft({ forceFail: true }).matchSnapshot() - expect.soft('I also have a matching snapshot').toMatchSnapshot() - expect.soft({ forceFail: true }).matchSnapshot() - - expect(getCurrentTest()?.result?.errors).toHaveLength(2) - expect(getCurrentTest()?.result?.state).toBe('fail') - passTestIfInFailedStateWithErrors() -}) - -test('expect.soft tracks errors for toMatchFileSnapshot', async () => { - await expect.soft({ forceFail: true }).toMatchFileSnapshot('fake-path/to-non-existent/file') - expect.soft('Another matching snapshot').toMatchSnapshot() - await expect.soft({ forceFail: true }).toMatchFileSnapshot('fake-path/to-non-existent/file') - - expect(getCurrentTest()?.result?.errors).toHaveLength(2) - expect(getCurrentTest()?.result?.state).toBe('fail') - passTestIfInFailedStateWithErrors() -}) - -test('expect.soft tracks errors for toThrowErrorMatchingSnapshot', async () => { - expect.soft({ forceFail: true }).toThrowErrorMatchingSnapshot() - expect.soft('Yet another matching snapshot').toMatchSnapshot() - expect.soft({ forceFail: true }).toThrowErrorMatchingSnapshot() - - expect(getCurrentTest()?.result?.errors).toHaveLength(2) - expect(getCurrentTest()?.result?.state).toBe('fail') - passTestIfInFailedStateWithErrors() -}) - -function passTestIfInFailedStateWithErrors() { - const result = getCurrentTest()!.result! - - if (result.errors && result.state === 'fail') { - result.state = 'pass' - } -} \ No newline at end of file diff --git a/test/snapshots/test/fixtures/test-update/soft-inline.test.ts b/test/snapshots/test/fixtures/test-update/soft-inline.test.ts deleted file mode 100644 index 00686358a0c4..000000000000 --- a/test/snapshots/test/fixtures/test-update/soft-inline.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { expect, test } from 'vitest' - -test('expect.soft fails if run with toMatchInlineSnapshot', () => { - let exceptionThrown = false - try { - expect.soft(1).toMatchInlineSnapshot(`1`) - } - catch (error: any) { - expect(error?.message).toContain('toMatchInlineSnapshot cannot be used with "soft"') - exceptionThrown = true - } - - expect(exceptionThrown).toBe(true) -}) - -test('expect.soft fails if run with toThrowErrorMatchingInlineSnapshot', () => { - let exceptionThrown = false - try { - expect.soft(() => { - throw new Error('1') - }).toThrowErrorMatchingInlineSnapshot(`[Error: 2]`) - } - catch (error: any) { - expect(error?.message).toContain('toThrowErrorMatchingInlineSnapshot cannot be used with "soft"') - exceptionThrown = true - } - - expect(exceptionThrown).toBe(true) -}) From 10b29145bac367ce4ea8c283523de11896269708 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 3 Feb 2026 18:52:52 +0900 Subject: [PATCH 07/32] test: update --- .../__snapshots__/soft-file.test.ts.snap | 13 -- test/snapshots/test-update/soft-file.test.ts | 78 ----------- .../snapshots/test-update/soft-inline.test.ts | 29 ---- .../__snapshots__/test-update.test.ts.snap | 127 ++++++++++++++++++ 4 files changed, 127 insertions(+), 120 deletions(-) delete mode 100644 test/snapshots/test-update/__snapshots__/soft-file.test.ts.snap delete mode 100644 test/snapshots/test-update/soft-file.test.ts delete mode 100644 test/snapshots/test-update/soft-inline.test.ts create mode 100644 test/snapshots/test/__snapshots__/test-update.test.ts.snap diff --git a/test/snapshots/test-update/__snapshots__/soft-file.test.ts.snap b/test/snapshots/test-update/__snapshots__/soft-file.test.ts.snap deleted file mode 100644 index 24d2f60ee474..000000000000 --- a/test/snapshots/test-update/__snapshots__/soft-file.test.ts.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`expect.soft tracks errors across successes for toMatchSnapshot 1`] = `"I have a matching snapshot"`; - -exports[`expect.soft tracks errors across successes for toMatchSnapshot 3`] = `"I also have a matching snapshot"`; - -exports[`expect.soft tracks errors across successes for toMatchSnapshot 5`] = `"Yet another matching snapshot"`; - -exports[`expect.soft tracks errors for matchSnapshot 2`] = `"I also have a matching snapshot"`; - -exports[`expect.soft tracks errors for toMatchFileSnapshot 2`] = `"Another matching snapshot"`; - -exports[`expect.soft tracks errors for toThrowErrorMatchingSnapshot 1`] = `"Yet another matching snapshot"`; diff --git a/test/snapshots/test-update/soft-file.test.ts b/test/snapshots/test-update/soft-file.test.ts deleted file mode 100644 index c36e9c5bcc95..000000000000 --- a/test/snapshots/test-update/soft-file.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { NewPlugin } from '@vitest/pretty-format' -import { getCurrentTest } from '@vitest/runner' -import { expect, test } from 'vitest' - -const AlwaysFailingSnapshotSerializer: NewPlugin = { - test: obj => obj && obj.forceFail === true, - serialize: () => { - throw new Error('Intentional snapshot serialization failure for testing') - }, -} -expect.addSnapshotSerializer(AlwaysFailingSnapshotSerializer) - -test('expect.soft tracks an error for toMatchSnapshot', () => { - expect.soft({ forceFail: true }).toMatchSnapshot() - - expect(getCurrentTest()?.result?.errors).toHaveLength(1) - expect(getCurrentTest()?.result?.state).toBe('fail') - passTestIfInFailedStateWithErrors() -}) - -test('expect.soft tracks multiple errors for toMatchSnapshot', () => { - expect.soft({ forceFail: true }).toMatchSnapshot() - expect.soft({ forceFail: true }).toMatchSnapshot() - - expect(getCurrentTest()?.result?.errors).toHaveLength(2) - expect(getCurrentTest()?.result?.state).toBe('fail') - passTestIfInFailedStateWithErrors() -}) - -test('expect.soft tracks errors across successes for toMatchSnapshot', () => { - expect.soft('I have a matching snapshot').toMatchSnapshot() - expect.soft({ forceFail: true }).toMatchSnapshot() - expect.soft('I also have a matching snapshot').toMatchSnapshot() - expect.soft({ forceFail: true }).toMatchSnapshot() - expect.soft('Yet another matching snapshot').toMatchSnapshot() - - expect(getCurrentTest()?.result?.errors).toHaveLength(2) - expect(getCurrentTest()?.result?.state).toBe('fail') - passTestIfInFailedStateWithErrors() -}) - -test('expect.soft tracks errors for matchSnapshot', () => { - expect.soft({ forceFail: true }).matchSnapshot() - expect.soft('I also have a matching snapshot').toMatchSnapshot() - expect.soft({ forceFail: true }).matchSnapshot() - - expect(getCurrentTest()?.result?.errors).toHaveLength(2) - expect(getCurrentTest()?.result?.state).toBe('fail') - passTestIfInFailedStateWithErrors() -}) - -test('expect.soft tracks errors for toMatchFileSnapshot', async () => { - await expect.soft({ forceFail: true }).toMatchFileSnapshot('fake-path/to-non-existent/file') - expect.soft('Another matching snapshot').toMatchSnapshot() - await expect.soft({ forceFail: true }).toMatchFileSnapshot('fake-path/to-non-existent/file') - - expect(getCurrentTest()?.result?.errors).toHaveLength(2) - expect(getCurrentTest()?.result?.state).toBe('fail') - passTestIfInFailedStateWithErrors() -}) - -test('expect.soft tracks errors for toThrowErrorMatchingSnapshot', async () => { - expect.soft({ forceFail: true }).toThrowErrorMatchingSnapshot() - expect.soft('Yet another matching snapshot').toMatchSnapshot() - expect.soft({ forceFail: true }).toThrowErrorMatchingSnapshot() - - expect(getCurrentTest()?.result?.errors).toHaveLength(2) - expect(getCurrentTest()?.result?.state).toBe('fail') - passTestIfInFailedStateWithErrors() -}) - -function passTestIfInFailedStateWithErrors() { - const result = getCurrentTest()!.result! - - if (result.errors && result.state === 'fail') { - result.state = 'pass' - } -} diff --git a/test/snapshots/test-update/soft-inline.test.ts b/test/snapshots/test-update/soft-inline.test.ts deleted file mode 100644 index 00686358a0c4..000000000000 --- a/test/snapshots/test-update/soft-inline.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { expect, test } from 'vitest' - -test('expect.soft fails if run with toMatchInlineSnapshot', () => { - let exceptionThrown = false - try { - expect.soft(1).toMatchInlineSnapshot(`1`) - } - catch (error: any) { - expect(error?.message).toContain('toMatchInlineSnapshot cannot be used with "soft"') - exceptionThrown = true - } - - expect(exceptionThrown).toBe(true) -}) - -test('expect.soft fails if run with toThrowErrorMatchingInlineSnapshot', () => { - let exceptionThrown = false - try { - expect.soft(() => { - throw new Error('1') - }).toThrowErrorMatchingInlineSnapshot(`[Error: 2]`) - } - catch (error: any) { - expect(error?.message).toContain('toThrowErrorMatchingInlineSnapshot cannot be used with "soft"') - exceptionThrown = true - } - - expect(exceptionThrown).toBe(true) -}) diff --git a/test/snapshots/test/__snapshots__/test-update.test.ts.snap b/test/snapshots/test/__snapshots__/test-update.test.ts.snap new file mode 100644 index 000000000000..3321ee7ef68e --- /dev/null +++ b/test/snapshots/test/__snapshots__/test-update.test.ts.snap @@ -0,0 +1,127 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`inline.test.js 1`] = ` +"import { describe, expect, test } from 'vitest' + +// when snapshots are generated Vitest reruns \`toMatchInlineSnapshot\` checks +// please, don't commit generated snapshots +describe('snapshots are generated in correct order', async () => { + test('first snapshot', () => { + expect({ foo: ['bar'] }).toMatchInlineSnapshot(\` + Object { + "foo": Array [ + "bar", + ], + } + \`) + }) + + test('second snapshot', () => { + expect({ foo: ['zed'] }).toMatchInlineSnapshot(\` + Object { + "foo": Array [ + "zed", + ], + } + \`) + }) +}) + +describe('snapshots with properties', () => { + test('without snapshot', () => { + expect({ foo: 'bar' }).toMatchInlineSnapshot({ foo: expect.any(String) }, \` + Object { + "foo": Any, + } + \`) + }) + + test('with snapshot', () => { + expect({ first: { second: { foo: 'bar' } } }).toMatchInlineSnapshot({ first: { second: { foo: expect.any(String) } } }, \` + Object { + "first": Object { + "second": Object { + "foo": Any, + }, + }, + } + \`) + }) + + test('mixed with and without snapshot', () => { + expect({ first: { second: { foo: 'bar' } } }).toMatchInlineSnapshot({ first: { second: { foo: expect.any(String) } } }, \` + Object { + "first": Object { + "second": Object { + "foo": Any, + }, + }, + } + \`) + + expect({ first: { second: { foo: 'zed' } } }).toMatchInlineSnapshot(\` + Object { + "first": Object { + "second": Object { + "foo": "zed", + }, + }, + } + \`) + }) +}) +" +`; + +exports[`inline-concurrent.test.js 1`] = ` +"import { it } from 'vitest' + +it.concurrent('1st', ({ expect }) => { + expect('hi1').toMatchInlineSnapshot(\`"hi1"\`) +}) + +it.concurrent('2nd', ({ expect }) => { + expect('hi2').toMatchInlineSnapshot(\`"hi2"\`) +}) + +it.concurrent('3rd', ({ expect }) => { + expect('hi3').toMatchInlineSnapshot(\`"hi3"\`) +}) +" +`; + +exports[`retry-inline.test.ts 1`] = ` +"import { expect, test } from 'vitest' + +test('inline repeats', { repeats: 1 }, () => { + expect('foo').toMatchInlineSnapshot(\`"foo"\`) +}) + +test('inline retry', { retry: 1 }, (ctx) => { + expect('foo').toMatchInlineSnapshot(\`"foo"\`) + if (ctx.task.result?.retryCount === 0) { + throw new Error('boom') + } +}) +" +`; + +exports[`same-title-inline.test.js 1`] = ` +"import { expect, test } from 'vitest' + +test('same title', () => { + expect('new').toMatchInlineSnapshot(\`"new"\`) + expect('new').toMatchInlineSnapshot(\`"new"\`) +}) + +test('same title', () => { + expect('a').toMatchInlineSnapshot(\`"a"\`) + expect('a').toMatchInlineSnapshot(\`"a"\`) +}) + +test('same title', () => { + expect('b').toMatchInlineSnapshot(\`"b"\`) + expect('b').toMatchInlineSnapshot(\`"b"\`) +}) +" +`; From ad74f7e4b8552a307590f1a9b3c68e52cfaf4507 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 3 Feb 2026 18:54:12 +0900 Subject: [PATCH 08/32] test: update --- test/snapshots/test/soft.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/snapshots/test/soft.test.ts b/test/snapshots/test/soft.test.ts index b896e7d1bdf9..d123a5449ae5 100644 --- a/test/snapshots/test/soft.test.ts +++ b/test/snapshots/test/soft.test.ts @@ -3,7 +3,7 @@ import { join } from 'node:path' import { expect, test } from 'vitest' import { editFile, runVitest } from '../../test-utils' -test('soft snapshot failures in read-only mode', async () => { +test('soft', async () => { const root = join(import.meta.dirname, 'fixtures/soft') const testFile = join(root, 'basic.test.ts') const snapshotFile = join(root, '__snapshots__/basic.test.ts.snap') From 4e43f49db8ec3a814be9a64cba1038674d06c493 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 3 Feb 2026 19:01:12 +0900 Subject: [PATCH 09/32] test: test toThrowErrorMatchingInlineSnapshot --- test/snapshots/test/soft-inline.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/snapshots/test/soft-inline.test.ts b/test/snapshots/test/soft-inline.test.ts index 29b1b6ae675d..f7e4ab76a346 100644 --- a/test/snapshots/test/soft-inline.test.ts +++ b/test/snapshots/test/soft-inline.test.ts @@ -4,4 +4,8 @@ test('not supported yet', () => { expect(() => expect.soft('test').toMatchInlineSnapshot()).toThrowErrorMatchingInlineSnapshot( `[Error: toMatchInlineSnapshot cannot be used with "soft"]`, ) + + expect(() => expect.soft(() => {}).toThrowErrorMatchingInlineSnapshot()).toThrowErrorMatchingInlineSnapshot( + `[Error: toThrowErrorMatchingInlineSnapshot cannot be used with "soft"]`, + ) }) From ffa7cf4abd6d711a288824d19b4860e48fae6b18 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 3 Feb 2026 19:11:32 +0900 Subject: [PATCH 10/32] wip: is this needed? --- packages/vitest/src/integrations/snapshot/chai.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 3f2a0fcacce2..60d038eaf785 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -115,11 +115,11 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { ...getTestNames(test), }) - const isSoft = utils.flag(this, 'soft') - if (isSoft) { - // assert result with soft wrapAssertion instead of recordAsyncExpect - return promise - } + // const isSoft = utils.flag(this, 'soft') + // if (isSoft) { + // // assert result with soft wrapAssertion instead of recordAsyncExpect + // return promise + // } return recordAsyncExpect( test, From a7305c109baa6fa1d6e20068dc59eed0efa1d836 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 09:11:03 +0900 Subject: [PATCH 11/32] chore: comment --- packages/vitest/src/integrations/snapshot/chai.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 60d038eaf785..4bb714694418 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -115,12 +115,9 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { ...getTestNames(test), }) - // const isSoft = utils.flag(this, 'soft') - // if (isSoft) { - // // assert result with soft wrapAssertion instead of recordAsyncExpect - // return promise - // } - + // TODO: + // `wrapAssertion` by `expect.soft` will call `promise.then` automatically, + // so `recordAsyncExpect` cannot track hanging promise. return recordAsyncExpect( test, promise, From 686f7dafeb2eec412be8cc34305b1c23b63f6030 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 09:24:48 +0900 Subject: [PATCH 12/32] chore: cleanup unused --- test/snapshots/tools/generate-inline-test.mjs | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 test/snapshots/tools/generate-inline-test.mjs diff --git a/test/snapshots/tools/generate-inline-test.mjs b/test/snapshots/tools/generate-inline-test.mjs deleted file mode 100644 index 3e775e092a71..000000000000 --- a/test/snapshots/tools/generate-inline-test.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import fs from 'node:fs/promises' -import { fileURLToPath } from 'node:url' -import { dirname, resolve } from 'pathe' - -const dir = dirname(fileURLToPath(import.meta.url)) - -export async function generateInlineTest(templatePath, testPath) { - const template = await fs.readFile(templatePath, 'utf8') - await fs.writeFile(testPath, template) - console.warn(`Generated ${testPath}`) -} - -const filepath = resolve(dir, '../test-update/snapshots-inline-js.test.js') -const template = resolve(dir, './inline-test-template.js'); - -(async () => { - await generateInlineTest(template, filepath) - await generateInlineTest( - resolve(dir, './inline-test-template-concurrent.js'), - resolve(dir, '../test-update/inline-test-template-concurrent.test.js'), - ) -})() From bc08a4cdf147fc8696cc4dd8080983e1f0418248 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 09:28:24 +0900 Subject: [PATCH 13/32] chore(test/snapshots): document test infra and rename test:snaps to test:integration - Add comprehensive README explaining the meta-testing architecture - Rename test:snaps to test:integration for clarity Co-Authored-By: Claude Opus 4.5 --- test/snapshots/README.md | 56 ++++++++++++++++++++++++++++++++++++- test/snapshots/package.json | 4 +-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/test/snapshots/README.md b/test/snapshots/README.md index 2c327219eb16..a2f4427e10fc 100644 --- a/test/snapshots/README.md +++ b/test/snapshots/README.md @@ -1 +1,55 @@ -TODO: explain which test script does what. +# Snapshot Tests + +This directory contains integration tests for Vitest's snapshot functionality. It uses a meta-testing approach where integration tests programmatically run fixture tests to validate snapshot behavior. + +## Directory Structure + +``` +test/snapshots/ +├── test/ # Integration tests that validate snapshot features +│ └── fixtures/ # Test fixture files (copied to test-update/) +├── test-update/ # Generated directory - populated from fixtures +├── generate.mjs # Resets test-update/ from fixtures +└── vitest.config.ts # Test configuration +``` + +## Test Scripts + +| Script | Purpose | +|--------|---------| +| `test` | Runs the complete test suite (all scripts below in sequence) | +| `test:generate` | Resets `test-update/` by copying fresh fixtures | +| `test:update` | Runs tests with `-u` flag to update existing snapshots | +| `test:update-new` | Runs with `CI=false` to create new snapshots | +| `test:update-none` | Runs with `CI=true` to validate without updates (strict mode) | +| `test:integration` | Runs the main integration tests in `test/` | + +## How It Works + +1. **`generate.mjs`** copies fixture files from `test/fixtures/test-update/` to `test-update/` +2. **`test:update*` scripts** run the fixture tests with different snapshot update modes +3. **`test:integration`** runs integration tests that use `runVitest()` to programmatically execute fixtures and assert on the results + +This setup allows testing snapshot features like: +- Inline snapshots (`toMatchInlineSnapshot`) +- File-based snapshots (`toMatchFileSnapshot`) +- Snapshot update behavior with `-u` flag +- CI vs non-CI snapshot creation modes +- Custom serializers, soft assertions, retry logic, etc. + +## Running Tests + +```bash +# Run all snapshot tests +pnpm test + +# Or run individual stages +# - Reset fixtures +pnpm test:generate +# - Run integration tests only +pnpm test:integration +pnpm test:integration test/summary.test.ts + +# Run one of fixtures directly +pnpm test:fixtures --root test/fixtures/summary +``` diff --git a/test/snapshots/package.json b/test/snapshots/package.json index 2a86029d5d7f..4f216938bb8c 100644 --- a/test/snapshots/package.json +++ b/test/snapshots/package.json @@ -3,9 +3,9 @@ "type": "module", "private": true, "scripts": { - "test": "pnpm run test:generate && pnpm run test:update && pnpm test:update-new && pnpm test:update-none && pnpm run test:snaps", + "test": "pnpm run test:generate && pnpm run test:update && pnpm test:update-new && pnpm test:update-none && pnpm run test:integration", "test:generate": "node ./generate.mjs", - "test:snaps": "vitest run --dir test", + "test:integration": "vitest run --dir test", "test:update": "vitest run -u --dir test-update", "test:update-none": "CI=true vitest run --dir test-update", "test:update-new": "CI=false vitest run --dir test-update", From d49fb31aeb3427f818096dce218cd963e965a9b4 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 10:22:28 +0900 Subject: [PATCH 14/32] refactor: reduce types --- packages/vitest/src/integrations/snapshot/chai.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 4bb714694418..d318eec4d5dd 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -63,7 +63,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { for (const key of ['matchSnapshot', 'toMatchSnapshot']) { utils.addMethod(chai.Assertion.prototype, key, wrapAssertion(utils, key, function ( - this: Chai.AssertionStatic & Chai.Assertion, + this, properties?: object, message?: string, ) { @@ -93,7 +93,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { utils.addMethod( chai.Assertion.prototype, 'toMatchFileSnapshot', - wrapAssertion(utils, 'toMatchFileSnapshot', function (this: Chai.AssertionStatic & Chai.Assertion, file: string, message?: string) { + wrapAssertion(utils, 'toMatchFileSnapshot', function (this, file: string, message?: string) { utils.flag(this, '_name', 'toMatchFileSnapshot') const isNot = utils.flag(this, 'negate') if (isNot) { @@ -179,7 +179,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { utils.addMethod( chai.Assertion.prototype, 'toThrowErrorMatchingSnapshot', - wrapAssertion(utils, 'toThrowErrorMatchingSnapshot', function (this: Chai.AssertionStatic & Chai.Assertion, properties?: object, message?: string) { + wrapAssertion(utils, 'toThrowErrorMatchingSnapshot', function (this, properties?: object, message?: string) { utils.flag(this, '_name', 'toThrowErrorMatchingSnapshot') const isNot = utils.flag(this, 'negate') if (isNot) { From 71449ab0ebff2b3d5e963a89434216a7902b375b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 10:54:35 +0900 Subject: [PATCH 15/32] fix: support soft inline snapshot --- .../vitest/src/integrations/snapshot/chai.ts | 41 +++++++++++++------ test/snapshots/test/soft-inline.test.ts | 3 +- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index d318eec4d5dd..4ac5cf4aaf93 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -127,11 +127,11 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { }), ) - utils.addMethod( - chai.Assertion.prototype, + const toMatchInlineSnapshotImpl = wrapAssertion( + utils, 'toMatchInlineSnapshot', - function __INLINE_SNAPSHOT__( - this: Record, + function ( + this, properties?: object, inlineSnapshot?: string, message?: string, @@ -141,10 +141,6 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { if (isNot) { throw new Error('toMatchInlineSnapshot cannot be used with "not"') } - const isSoft = utils.flag(this, 'soft') - if (isSoft) { - throw new Error('toMatchInlineSnapshot cannot be used with "soft"') - } const test = getTest('toMatchInlineSnapshot', this) const isInsideEach = test.each || test.suite?.each if (isInsideEach) { @@ -176,6 +172,16 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { }) }, ) + Object.defineProperty(toMatchInlineSnapshotImpl, 'name', { + value: '__INLINE_SNAPSHOT__', + configurable: true, + }) + utils.addMethod( + chai.Assertion.prototype, + 'toMatchInlineSnapshot', + toMatchInlineSnapshotImpl, + ) + utils.addMethod( chai.Assertion.prototype, 'toThrowErrorMatchingSnapshot', @@ -199,11 +205,12 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { }) }), ) - utils.addMethod( - chai.Assertion.prototype, + + const toThrowErrorMatchingInlineSnapshotImpl = wrapAssertion( + utils, 'toThrowErrorMatchingInlineSnapshot', - function __INLINE_SNAPSHOT__( - this: Record, + function ( + this, inlineSnapshot: string, message: string, ) { @@ -244,5 +251,15 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { }) }, ) + Object.defineProperty(toThrowErrorMatchingInlineSnapshotImpl, 'name', { + value: '__INLINE_SNAPSHOT__', + configurable: true, + }) + utils.addMethod( + chai.Assertion.prototype, + 'toThrowErrorMatchingInlineSnapshot', + toThrowErrorMatchingInlineSnapshotImpl, + ) + utils.addMethod(chai.expect, 'addSnapshotSerializer', addSerializer) } diff --git a/test/snapshots/test/soft-inline.test.ts b/test/snapshots/test/soft-inline.test.ts index f7e4ab76a346..bed8bf47aad3 100644 --- a/test/snapshots/test/soft-inline.test.ts +++ b/test/snapshots/test/soft-inline.test.ts @@ -1,6 +1,7 @@ import { expect, test } from 'vitest' -test('not supported yet', () => { +// TODO +test.skip('not supported yet', () => { expect(() => expect.soft('test').toMatchInlineSnapshot()).toThrowErrorMatchingInlineSnapshot( `[Error: toMatchInlineSnapshot cannot be used with "soft"]`, ) From bd60ae4db7ec3523a6412308d239a30511005c18 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 10:58:05 +0900 Subject: [PATCH 16/32] fix: allow soft --- packages/vitest/src/integrations/snapshot/chai.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 4ac5cf4aaf93..355ff0840d05 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -220,10 +220,6 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { 'toThrowErrorMatchingInlineSnapshot cannot be used with "not"', ) } - const isSoft = utils.flag(this, 'soft') - if (isSoft) { - throw new Error('toThrowErrorMatchingInlineSnapshot cannot be used with "soft"') - } const test = getTest('toThrowErrorMatchingInlineSnapshot', this) const isInsideEach = test.each || test.suite?.each if (isInsideEach) { From d3ac41b4048846e1d15010a677a956677b23a897 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 11:04:05 +0900 Subject: [PATCH 17/32] chore: comment --- packages/vitest/src/integrations/snapshot/chai.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 355ff0840d05..fa672e980499 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -116,8 +116,8 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { }) // TODO: - // `wrapAssertion` by `expect.soft` will call `promise.then` automatically, - // so `recordAsyncExpect` cannot track hanging promise. + // `wrapAssertion` calls `promise.then` automatically, + // so `recordAsyncExpect`'s hanging promise detection doesn't work. return recordAsyncExpect( test, promise, From b24963b1b612f0b0b554cfe110d69047abc25545 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 11:23:10 +0900 Subject: [PATCH 18/32] test: test soft inline snapshot --- .../test/fixtures/soft-inline/basic.test.ts | 11 +++ test/snapshots/test/soft-inline.test.ts | 93 +++++++++++++++++-- test/snapshots/test/soft.test.ts | 5 +- 3 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 test/snapshots/test/fixtures/soft-inline/basic.test.ts diff --git a/test/snapshots/test/fixtures/soft-inline/basic.test.ts b/test/snapshots/test/fixtures/soft-inline/basic.test.ts new file mode 100644 index 000000000000..b46e53389bad --- /dev/null +++ b/test/snapshots/test/fixtures/soft-inline/basic.test.ts @@ -0,0 +1,11 @@ +import { expect, test } from 'vitest' + +test('toMatchInlineSnapshot', () => { + expect.soft('--snap-1--').toMatchInlineSnapshot(`"--snap-1--"`) + expect.soft('--snap-2--').toMatchInlineSnapshot(`"--snap-2--"`) +}) + +test('toThrowErrorMatchingInlineSnapshot', () => { + expect.soft(() => { throw new Error('--error-1--') }).toThrowErrorMatchingInlineSnapshot(`[Error: --error-1--]`) + expect.soft(() => { throw new Error('--error-2--') }).toThrowErrorMatchingInlineSnapshot(`[Error: --error-2--]`) +}) diff --git a/test/snapshots/test/soft-inline.test.ts b/test/snapshots/test/soft-inline.test.ts index bed8bf47aad3..14a723640388 100644 --- a/test/snapshots/test/soft-inline.test.ts +++ b/test/snapshots/test/soft-inline.test.ts @@ -1,12 +1,89 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' import { expect, test } from 'vitest' +import { editFile, runVitest } from '../../test-utils' -// TODO -test.skip('not supported yet', () => { - expect(() => expect.soft('test').toMatchInlineSnapshot()).toThrowErrorMatchingInlineSnapshot( - `[Error: toMatchInlineSnapshot cannot be used with "soft"]`, - ) +test('soft inline', async () => { + const root = join(import.meta.dirname, 'fixtures/soft-inline') + const testFile = join(root, 'basic.test.ts') - expect(() => expect.soft(() => {}).toThrowErrorMatchingInlineSnapshot()).toThrowErrorMatchingInlineSnapshot( - `[Error: toThrowErrorMatchingInlineSnapshot cannot be used with "soft"]`, - ) + // remove inline snapshots + editFile(testFile, s => s + .replace(/toMatchInlineSnapshot\(`[^`]*`\)/g, 'toMatchInlineSnapshot()') + .replace(/toThrowErrorMatchingInlineSnapshot\(`[^`]*`\)/g, 'toThrowErrorMatchingInlineSnapshot()')) + + // create snapshots from scratch + let result = await runVitest({ root, update: 'new' }) + expect(result.stderr).toMatchInlineSnapshot(`""`) + expect(readFileSync(testFile, 'utf-8')).toMatchInlineSnapshot(` + "import { expect, test } from 'vitest' + + test('toMatchInlineSnapshot', () => { + expect.soft('--snap-1--').toMatchInlineSnapshot(\`"--snap-1--"\`) + expect.soft('--snap-2--').toMatchInlineSnapshot(\`"--snap-2--"\`) + }) + + test('toThrowErrorMatchingInlineSnapshot', () => { + expect.soft(() => { throw new Error('--error-1--') }).toThrowErrorMatchingInlineSnapshot(\`[Error: --error-1--]\`) + expect.soft(() => { throw new Error('--error-2--') }).toThrowErrorMatchingInlineSnapshot(\`[Error: --error-2--]\`) + }) + " + `) + expect(result.errorTree()).toMatchInlineSnapshot(` + Object { + "basic.test.ts": Object { + "toMatchInlineSnapshot": "passed", + "toThrowErrorMatchingInlineSnapshot": "passed", + }, + } + `) + + // edit tests to introduce snapshot errors + editFile(testFile, s => s + .replace("expect.soft('--snap-1--')", "expect.soft('--snap-1-edit--')") + .replace("expect.soft('--snap-2--')", "expect.soft('--snap-2-edit--')") + .replace("new Error('--error-1--')", "new Error('--error-1-edit--')") + .replace("new Error('--error-2--')", "new Error('--error-2-edit--')")) + + result = await runVitest({ root, update: false }) + expect(result.errorTree()).toMatchInlineSnapshot(` + Object { + "basic.test.ts": Object { + "toMatchInlineSnapshot": Array [ + "Snapshot \`toMatchInlineSnapshot 1\` mismatched", + "Snapshot \`toMatchInlineSnapshot 2\` mismatched", + ], + "toThrowErrorMatchingInlineSnapshot": Array [ + "Snapshot \`toThrowErrorMatchingInlineSnapshot 1\` mismatched", + "Snapshot \`toThrowErrorMatchingInlineSnapshot 2\` mismatched", + ], + }, + } + `) + + // run with update + result = await runVitest({ root, update: 'all' }) + expect(result.stderr).toMatchInlineSnapshot(`""`) + expect(readFileSync(testFile, 'utf-8')).toMatchInlineSnapshot(` + "import { expect, test } from 'vitest' + + test('toMatchInlineSnapshot', () => { + expect.soft('--snap-1-edit--').toMatchInlineSnapshot(\`"--snap-1-edit--"\`) + expect.soft('--snap-2-edit--').toMatchInlineSnapshot(\`"--snap-2-edit--"\`) + }) + + test('toThrowErrorMatchingInlineSnapshot', () => { + expect.soft(() => { throw new Error('--error-1-edit--') }).toThrowErrorMatchingInlineSnapshot(\`[Error: --error-1-edit--]\`) + expect.soft(() => { throw new Error('--error-2-edit--') }).toThrowErrorMatchingInlineSnapshot(\`[Error: --error-2-edit--]\`) + }) + " + `) + expect(result.errorTree()).toMatchInlineSnapshot(` + Object { + "basic.test.ts": Object { + "toMatchInlineSnapshot": "passed", + "toThrowErrorMatchingInlineSnapshot": "passed", + }, + } + `) }) diff --git a/test/snapshots/test/soft.test.ts b/test/snapshots/test/soft.test.ts index d123a5449ae5..8692ab4fad3a 100644 --- a/test/snapshots/test/soft.test.ts +++ b/test/snapshots/test/soft.test.ts @@ -10,8 +10,10 @@ test('soft', async () => { const customFile1 = join(root, '__snapshots__/custom1.txt') const customFile2 = join(root, '__snapshots__/custom2.txt') - // reset and create snapshots from scratch + // remove snapshots fs.rmSync(join(root, '__snapshots__'), { recursive: true, force: true }) + + // create snapshots from scratch let result = await runVitest({ root, update: 'new' }) expect(result.stderr).toMatchInlineSnapshot(`""`) expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` @@ -46,6 +48,7 @@ test('soft', async () => { .replace(`--file-2--`, `--file-2-edit--`) .replace(`--error-1--`, `--error-1-edit--`) .replace(`--error-2--`, `--error-2-edit--`)) + result = await runVitest({ root, update: false }) expect(result.stderr).toMatchInlineSnapshot(` " From b65a9860b933448bc4c7336b006877054f24eb15 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 11:33:27 +0900 Subject: [PATCH 19/32] fix: use __INLINE_SNAPSHOT_OFFSET_3__ to tweak stack --- packages/snapshot/src/port/state.ts | 9 +++++++++ packages/vitest/src/integrations/snapshot/chai.ts | 12 ++---------- test/snapshots/test/soft-inline.test.ts | 8 ++++---- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/snapshot/src/port/state.ts b/packages/snapshot/src/port/state.ts index e691822ba8d4..b197bc577e50 100644 --- a/packages/snapshot/src/port/state.ts +++ b/packages/snapshot/src/port/state.ts @@ -168,6 +168,15 @@ export default class SnapshotState { return stacks[promiseIndex + 3] } + // inline snapshot function can be named __INLINE_SNAPSHOT_OFFSET___ + // to specify a custom stack offset + for (let i = 0; i < stacks.length; i++) { + const match = stacks[i].method.match(/__INLINE_SNAPSHOT_OFFSET_(\d+)__/) + if (match) { + return stacks[i + Number(match[1])] ?? null + } + } + // inline snapshot function is called __INLINE_SNAPSHOT__ // in integrations/snapshot/chai.ts const stackIndex = stacks.findIndex(i => diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index fa672e980499..b1d5be58b411 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -130,7 +130,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { const toMatchInlineSnapshotImpl = wrapAssertion( utils, 'toMatchInlineSnapshot', - function ( + function __INLINE_SNAPSHOT_OFFSET_3__( this, properties?: object, inlineSnapshot?: string, @@ -172,10 +172,6 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { }) }, ) - Object.defineProperty(toMatchInlineSnapshotImpl, 'name', { - value: '__INLINE_SNAPSHOT__', - configurable: true, - }) utils.addMethod( chai.Assertion.prototype, 'toMatchInlineSnapshot', @@ -209,7 +205,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { const toThrowErrorMatchingInlineSnapshotImpl = wrapAssertion( utils, 'toThrowErrorMatchingInlineSnapshot', - function ( + function __INLINE_SNAPSHOT_OFFSET_3__( this, inlineSnapshot: string, message: string, @@ -247,10 +243,6 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { }) }, ) - Object.defineProperty(toThrowErrorMatchingInlineSnapshotImpl, 'name', { - value: '__INLINE_SNAPSHOT__', - configurable: true, - }) utils.addMethod( chai.Assertion.prototype, 'toThrowErrorMatchingInlineSnapshot', diff --git a/test/snapshots/test/soft-inline.test.ts b/test/snapshots/test/soft-inline.test.ts index 14a723640388..a1050b9c9279 100644 --- a/test/snapshots/test/soft-inline.test.ts +++ b/test/snapshots/test/soft-inline.test.ts @@ -40,10 +40,10 @@ test('soft inline', async () => { // edit tests to introduce snapshot errors editFile(testFile, s => s - .replace("expect.soft('--snap-1--')", "expect.soft('--snap-1-edit--')") - .replace("expect.soft('--snap-2--')", "expect.soft('--snap-2-edit--')") - .replace("new Error('--error-1--')", "new Error('--error-1-edit--')") - .replace("new Error('--error-2--')", "new Error('--error-2-edit--')")) + .replace('expect.soft(\'--snap-1--\')', 'expect.soft(\'--snap-1-edit--\')') + .replace('expect.soft(\'--snap-2--\')', 'expect.soft(\'--snap-2-edit--\')') + .replace('new Error(\'--error-1--\')', 'new Error(\'--error-1-edit--\')') + .replace('new Error(\'--error-2--\')', 'new Error(\'--error-2-edit--\')')) result = await runVitest({ root, update: false }) expect(result.errorTree()).toMatchInlineSnapshot(` From c2a0c903d90ca9564457069a028f4592525be14d Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 12:31:57 +0900 Subject: [PATCH 20/32] fix: webkit ptc --- packages/expect/src/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index 13e994812b27..1f579166df72 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -93,7 +93,10 @@ export function wrapAssertion( } if (!utils.flag(this, 'soft')) { - return fn.apply(this, args) + // de-opt from WebKit's tail call optimization to ensure inlint snapshot stacktrace offset + // https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-in-webkit + const result = fn.apply(this, args) + return result } const test: Test = utils.flag(this, 'vitest-test') From add5a14f0af4b9070b8f1193e179da47922b3884 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 12:43:37 +0900 Subject: [PATCH 21/32] fix: use try/finally to prevent WebKit TCO optimization The previous `const result = fn.apply(); return result` pattern was being optimized back to a tail call by the bundler. try/finally survives transpilation and prevents WebKit PTC. Co-Authored-By: Claude Opus 4.5 --- packages/expect/src/utils.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index 1f579166df72..6a782e35a807 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -93,10 +93,14 @@ export function wrapAssertion( } if (!utils.flag(this, 'soft')) { - // de-opt from WebKit's tail call optimization to ensure inlint snapshot stacktrace offset + // de-opt from WebKit's tail call optimization to ensure inline snapshot stacktrace offset // https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-in-webkit - const result = fn.apply(this, args) - return result + try { + return fn.apply(this, args) + } + finally { + // do not remove - prevents removal from transpiler and WebKit TCO + } } const test: Test = utils.flag(this, 'vitest-test') From 445a8f44fe8167738ee7e20ce9f2df8aa69f7f95 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 12:48:11 +0900 Subject: [PATCH 22/32] chore: comment --- packages/expect/src/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index 6a782e35a807..46ff21ecfc7e 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -93,13 +93,13 @@ export function wrapAssertion( } if (!utils.flag(this, 'soft')) { - // de-opt from WebKit's tail call optimization to ensure inline snapshot stacktrace offset + // avoid WebKit's proper tail call to preserve stacktrace offset for inline snapshot // https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-in-webkit try { return fn.apply(this, args) } finally { - // do not remove - prevents removal from transpiler and WebKit TCO + // no lint } } From 834aed25a004fd9c5b5fbc3cf03e67d8a71e0268 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 13:44:28 +0900 Subject: [PATCH 23/32] chore: todo --- packages/vitest/src/integrations/snapshot/chai.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index b1d5be58b411..62a8bd4757d5 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -116,7 +116,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { }) // TODO: - // `wrapAssertion` calls `promise.then` automatically, + // `wrapAssertion` with `expect.soft` calls `promise.then` automatically, // so `recordAsyncExpect`'s hanging promise detection doesn't work. return recordAsyncExpect( test, From 9aad5a44490ce64c7c588f32e92d558e182aee1c Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 14:08:38 +0900 Subject: [PATCH 24/32] fix: make wrapAssertion and recordAsyncExpect work together --- packages/expect/src/utils.ts | 11 +++++++++++ packages/vitest/src/integrations/snapshot/chai.ts | 3 --- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index 46ff21ecfc7e..dafc4c4f21dd 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -15,6 +15,8 @@ export function createAssertionMessage( return `expect(actual)${promise}.${not}${name}` } +const RECORD_ASYNC_EXPECT_SYMBOL = Symbol.for('vitest.recordAsyncExpect') + export function recordAsyncExpect( _test: any, promise: Promise, @@ -68,6 +70,7 @@ export function recordAsyncExpect( return promise.finally(onFinally) }, [Symbol.toStringTag]: 'Promise', + [RECORD_ASYNC_EXPECT_SYMBOL as any]: true, } satisfies Promise } @@ -113,6 +116,14 @@ export function wrapAssertion( const result = fn.apply(this, args) if (result && typeof result === 'object' && typeof result.then === 'function') { + // don't invoke `then` on `recordAsyncExpect` wrapper + // to avoid breaking hanging promise detection + if (RECORD_ASYNC_EXPECT_SYMBOL in result) { + (result as any as Promise).catch((err) => { + handleTestError(test, err) + }) + return result + } return result.then(noop, (err) => { handleTestError(test, err) }) diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 62a8bd4757d5..ae38dd6ac9e6 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -115,9 +115,6 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { ...getTestNames(test), }) - // TODO: - // `wrapAssertion` with `expect.soft` calls `promise.then` automatically, - // so `recordAsyncExpect`'s hanging promise detection doesn't work. return recordAsyncExpect( test, promise, From dbd3bd346f1173f5fe4f217419adc7637a33eaed Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 14:14:49 +0900 Subject: [PATCH 25/32] test: test soft file snapshot warning --- test/cli/test/fails.test.ts | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/test/cli/test/fails.test.ts b/test/cli/test/fails.test.ts index 3113d215be18..fd5395ec5d4a 100644 --- a/test/cli/test/fails.test.ts +++ b/test/cli/test/fails.test.ts @@ -1,4 +1,3 @@ -import type { TestCase } from 'vitest/node' import { playwright } from '@vitest/browser-playwright' import { resolve } from 'pathe' @@ -50,8 +49,8 @@ it('should not report coverage when "coverage.reportOnFailure" has default value expect(stdout).not.toMatch('Coverage report from istanbul') }) -it('prints a warning if the assertion is not awaited', async () => { - const { stderr, results, root } = await runInlineTests({ +it.only('prints a warning if the assertion is not awaited', async () => { + const { stderr, results, root, errorTree } = await runInlineTests({ 'base.test.js': ts` import { expect, test } from 'vitest'; @@ -69,21 +68,28 @@ it('prints a warning if the assertion is not awaited', async () => { expect(1).toBe(2) }) - test('toMatchSnapshot not awaited', () => { + test('toMatchFileSnapshot not awaited', () => { expect(1).toMatchFileSnapshot('./snapshot.txt') }) + + test('soft + toMatchFileSnapshot not awaited', () => { + expect.soft(1).toMatchFileSnapshot('./snapshot-soft.txt') + }) `, }) - expect(results[0].children.size).toEqual(4) - const failedTest = results[0].children.at(2) as TestCase - expect(failedTest.result()).toEqual({ - state: 'failed', - errors: [ - expect.objectContaining({ - message: expect.stringContaining('expected 1 to be 2'), - }), - ], - }) + expect(errorTree()).toMatchInlineSnapshot(` + { + "base.test.js": { + "not awaited and failed": [ + "expected 1 to be 2 // Object.is equality", + ], + "several not awaited": "passed", + "single not awaited": "passed", + "soft + toMatchFileSnapshot not awaited": "passed", + "toMatchFileSnapshot not awaited": "passed", + }, + } + `) const warnings: string[] = [] const lines = stderr.split('\n') lines.forEach((line, index) => { @@ -103,6 +109,8 @@ it('prints a warning if the assertion is not awaited', async () => { at /base.test.js:14:33", "Promise returned by \`expect(actual).toMatchFileSnapshot(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in the next Vitest major. Please remember to await the assertion. at /base.test.js:19:17", + "Promise returned by \`expect(actual).toMatchFileSnapshot(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in the next Vitest major. Please remember to await the assertion. + at /base.test.js:23:22", ] `) }) From 59f03fa718136e5a5e093f6c39c5c10be978fc1e Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 14:18:21 +0900 Subject: [PATCH 26/32] fix: fix non awaited soft warning --- packages/expect/src/utils.ts | 3 ++- test/cli/test/fails.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index dafc4c4f21dd..40c1009fbd43 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -8,11 +8,12 @@ export function createAssertionMessage( assertion: Assertion, hasArgs: boolean, ) { + const soft = util.flag(assertion, 'soft') ? '.soft' : '' const not = util.flag(assertion, 'negate') ? 'not.' : '' const name = `${util.flag(assertion, '_name')}(${hasArgs ? 'expected' : ''})` const promiseName = util.flag(assertion, 'promise') const promise = promiseName ? `.${promiseName}` : '' - return `expect(actual)${promise}.${not}${name}` + return `expect${soft}(actual)${promise}.${not}${name}` } const RECORD_ASYNC_EXPECT_SYMBOL = Symbol.for('vitest.recordAsyncExpect') diff --git a/test/cli/test/fails.test.ts b/test/cli/test/fails.test.ts index fd5395ec5d4a..cab96ade6a5d 100644 --- a/test/cli/test/fails.test.ts +++ b/test/cli/test/fails.test.ts @@ -49,8 +49,8 @@ it('should not report coverage when "coverage.reportOnFailure" has default value expect(stdout).not.toMatch('Coverage report from istanbul') }) -it.only('prints a warning if the assertion is not awaited', async () => { - const { stderr, results, root, errorTree } = await runInlineTests({ +it('prints a warning if the assertion is not awaited', async () => { + const { stderr, root, errorTree } = await runInlineTests({ 'base.test.js': ts` import { expect, test } from 'vitest'; @@ -109,7 +109,7 @@ it.only('prints a warning if the assertion is not awaited', async () => { at /base.test.js:14:33", "Promise returned by \`expect(actual).toMatchFileSnapshot(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in the next Vitest major. Please remember to await the assertion. at /base.test.js:19:17", - "Promise returned by \`expect(actual).toMatchFileSnapshot(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in the next Vitest major. Please remember to await the assertion. + "Promise returned by \`expect.soft(actual).toMatchFileSnapshot(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in the next Vitest major. Please remember to await the assertion. at /base.test.js:23:22", ] `) From 893628eb160c7e65b5d7e13c0a65c648d6ffce85 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 14:25:14 +0900 Subject: [PATCH 27/32] chore: cleanup --- .../vitest/src/integrations/snapshot/chai.ts | 85 +++++++++---------- 1 file changed, 38 insertions(+), 47 deletions(-) diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index ae38dd6ac9e6..868aefd5f926 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -62,32 +62,36 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { } for (const key of ['matchSnapshot', 'toMatchSnapshot']) { - utils.addMethod(chai.Assertion.prototype, key, wrapAssertion(utils, key, function ( - this, - properties?: object, - message?: string, - ) { - utils.flag(this, '_name', key) - const isNot = utils.flag(this, 'negate') - if (isNot) { - throw new Error(`${key} cannot be used with "not"`) - } - const expected = utils.flag(this, 'object') - const test = getTest(key, this) - if (typeof properties === 'string' && typeof message === 'undefined') { - message = properties - properties = undefined - } - const errorMessage = utils.flag(this, 'message') - getSnapshotClient().assert({ - received: expected, - message, - isInline: false, - properties, - errorMessage, - ...getTestNames(test), - }) - })) + utils.addMethod( + chai.Assertion.prototype, + key, + wrapAssertion(utils, key, function ( + this, + properties?: object, + message?: string, + ) { + utils.flag(this, '_name', key) + const isNot = utils.flag(this, 'negate') + if (isNot) { + throw new Error(`${key} cannot be used with "not"`) + } + const expected = utils.flag(this, 'object') + const test = getTest(key, this) + if (typeof properties === 'string' && typeof message === 'undefined') { + message = properties + properties = undefined + } + const errorMessage = utils.flag(this, 'message') + getSnapshotClient().assert({ + received: expected, + message, + isInline: false, + properties, + errorMessage, + ...getTestNames(test), + }) + }), + ) } utils.addMethod( @@ -124,10 +128,10 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { }), ) - const toMatchInlineSnapshotImpl = wrapAssertion( - utils, + utils.addMethod( + chai.Assertion.prototype, 'toMatchInlineSnapshot', - function __INLINE_SNAPSHOT_OFFSET_3__( + wrapAssertion(utils, 'toMatchInlineSnapshot', function __INLINE_SNAPSHOT_OFFSET_3__( this, properties?: object, inlineSnapshot?: string, @@ -167,14 +171,8 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { errorMessage, ...getTestNames(test), }) - }, - ) - utils.addMethod( - chai.Assertion.prototype, - 'toMatchInlineSnapshot', - toMatchInlineSnapshotImpl, + }), ) - utils.addMethod( chai.Assertion.prototype, 'toThrowErrorMatchingSnapshot', @@ -198,11 +196,10 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { }) }), ) - - const toThrowErrorMatchingInlineSnapshotImpl = wrapAssertion( - utils, + utils.addMethod( + chai.Assertion.prototype, 'toThrowErrorMatchingInlineSnapshot', - function __INLINE_SNAPSHOT_OFFSET_3__( + wrapAssertion(utils, 'toThrowErrorMatchingInlineSnapshot', function __INLINE_SNAPSHOT_OFFSET_3__( this, inlineSnapshot: string, message: string, @@ -238,13 +235,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { errorMessage, ...getTestNames(test), }) - }, - ) - utils.addMethod( - chai.Assertion.prototype, - 'toThrowErrorMatchingInlineSnapshot', - toThrowErrorMatchingInlineSnapshotImpl, + }), ) - utils.addMethod(chai.expect, 'addSnapshotSerializer', addSerializer) } From 847a2bdd17887454d49407d4ac5eae39240eb16c Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 14:27:23 +0900 Subject: [PATCH 28/32] test: update --- test/cli/test/fails.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/cli/test/fails.test.ts b/test/cli/test/fails.test.ts index cab96ade6a5d..a20c6eb35a3f 100644 --- a/test/cli/test/fails.test.ts +++ b/test/cli/test/fails.test.ts @@ -76,6 +76,8 @@ it('prints a warning if the assertion is not awaited', async () => { expect.soft(1).toMatchFileSnapshot('./snapshot-soft.txt') }) `, + }, { + update: true, }) expect(errorTree()).toMatchInlineSnapshot(` { From a03645f35bd291bbad86dd88752c0860b4720343 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 15:51:55 +0900 Subject: [PATCH 29/32] fix: soft async tracking --- packages/expect/src/utils.ts | 19 +++---- .../vitest/src/integrations/snapshot/chai.ts | 1 + test/snapshots/test/soft.test.ts | 50 +++++++++---------- 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index 40c1009fbd43..e7b9b1fd423d 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -16,13 +16,12 @@ export function createAssertionMessage( return `expect${soft}(actual)${promise}.${not}${name}` } -const RECORD_ASYNC_EXPECT_SYMBOL = Symbol.for('vitest.recordAsyncExpect') - export function recordAsyncExpect( _test: any, promise: Promise, assertion: string, error: Error, + isSoft?: boolean, ): Promise { const test = _test as Test | undefined // record promise for test, that resolves before test ends @@ -42,6 +41,12 @@ export function recordAsyncExpect( if (!test.promises) { test.promises = [] } + // setup `expect.soft` handler instead of `wrapAssertion` to avoid double tracking + if (isSoft) { + promise = promise.then(noop, (err) => { + handleTestError(test, err) + }) + } test.promises.push(promise) let resolved = false @@ -71,7 +76,7 @@ export function recordAsyncExpect( return promise.finally(onFinally) }, [Symbol.toStringTag]: 'Promise', - [RECORD_ASYNC_EXPECT_SYMBOL as any]: true, + ['__vitest_async_soft__' as any]: isSoft, } satisfies Promise } @@ -117,12 +122,8 @@ export function wrapAssertion( const result = fn.apply(this, args) if (result && typeof result === 'object' && typeof result.then === 'function') { - // don't invoke `then` on `recordAsyncExpect` wrapper - // to avoid breaking hanging promise detection - if (RECORD_ASYNC_EXPECT_SYMBOL in result) { - (result as any as Promise).catch((err) => { - handleTestError(test, err) - }) + // don't invoke `then` on `expect.soft` to detect non-awaited promises + if ((result as any).__vitest_async_soft__) { return result } return result.then(noop, (err) => { diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 868aefd5f926..7fbf00f151a1 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -124,6 +124,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { promise, createAssertionMessage(utils, this as any, true), error, + utils.flag(this, 'soft'), ) }), ) diff --git a/test/snapshots/test/soft.test.ts b/test/snapshots/test/soft.test.ts index 8692ab4fad3a..1b06ad825094 100644 --- a/test/snapshots/test/soft.test.ts +++ b/test/snapshots/test/soft.test.ts @@ -92,13 +92,13 @@ test('soft', async () => { Expected: "--file-1--" Received: "--file-1-edit--" - ❯ basic.test.ts:9:3 - 7| - 8| test('toMatchFileSnapshot', async () => { - 9| await expect.soft('--file-1-edit--').toMatchFileSnapshot('./__snapsh… - | ^ - 10| await expect.soft('--file-2-edit--').toMatchFileSnapshot('./__snapsh… - 11| }) + ❯ basic.test.ts:13:5 + 11| // console.log("[e]", e) + 12| // } + 13| await expect.soft('--file-1-edit--').toMatchFileSnapshot('./__snap… + | ^ + 14| + 15| await expect.soft('--file-2-edit--').toMatchFileSnapshot('./__snapsh… ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/6]⎯ @@ -108,13 +108,13 @@ test('soft', async () => { Expected: "--file-2--" Received: "--file-2-edit--" - ❯ basic.test.ts:10:3 - 8| test('toMatchFileSnapshot', async () => { - 9| await expect.soft('--file-1-edit--').toMatchFileSnapshot('./__snapsh… - 10| await expect.soft('--file-2-edit--').toMatchFileSnapshot('./__snapsh… + ❯ basic.test.ts:15:3 + 13| await expect.soft('--file-1-edit--').toMatchFileSnapshot('./__snap… + 14| + 15| await expect.soft('--file-2-edit--').toMatchFileSnapshot('./__snapsh… | ^ - 11| }) - 12| + 16| }) + 17| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/6]⎯ @@ -124,13 +124,13 @@ test('soft', async () => { Expected: "[Error: --error-1--]" Received: "[Error: --error-1-edit--]" - ❯ basic.test.ts:14:62 - 12| - 13| test('toThrowErrorMatchingSnapshot', () => { - 14| expect.soft(() => { throw new Error('--error-1-edit--') }).toThrowEr… + ❯ basic.test.ts:19:62 + 17| + 18| test('toThrowErrorMatchingSnapshot', () => { + 19| expect.soft(() => { throw new Error('--error-1-edit--') }).toThrowEr… | ^ - 15| expect.soft(() => { throw new Error('--error-2-edit--') }).toThrowEr… - 16| }) + 20| expect.soft(() => { throw new Error('--error-2-edit--') }).toThrowEr… + 21| }) ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/6]⎯ @@ -140,13 +140,13 @@ test('soft', async () => { Expected: "[Error: --error-2--]" Received: "[Error: --error-2-edit--]" - ❯ basic.test.ts:15:62 - 13| test('toThrowErrorMatchingSnapshot', () => { - 14| expect.soft(() => { throw new Error('--error-1-edit--') }).toThrowEr… - 15| expect.soft(() => { throw new Error('--error-2-edit--') }).toThrowEr… + ❯ basic.test.ts:20:62 + 18| test('toThrowErrorMatchingSnapshot', () => { + 19| expect.soft(() => { throw new Error('--error-1-edit--') }).toThrowEr… + 20| expect.soft(() => { throw new Error('--error-2-edit--') }).toThrowEr… | ^ - 16| }) - 17| + 21| }) + 22| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/6]⎯ From 6b6bdf449d560eb1692b207033ecbd489e8af0a3 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 15:53:18 +0900 Subject: [PATCH 30/32] chore: comment --- packages/expect/src/utils.ts | 2 +- packages/vitest/src/integrations/snapshot/chai.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index e7b9b1fd423d..a987856f180f 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -41,7 +41,7 @@ export function recordAsyncExpect( if (!test.promises) { test.promises = [] } - // setup `expect.soft` handler instead of `wrapAssertion` to avoid double tracking + // setup `expect.soft` handler here instead of `wrapAssertion` to avoid double tracking if (isSoft) { promise = promise.then(noop, (err) => { handleTestError(test, err) diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 7fbf00f151a1..8bb65d02cdfa 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -122,7 +122,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { return recordAsyncExpect( test, promise, - createAssertionMessage(utils, this as any, true), + createAssertionMessage(utils, this, true), error, utils.flag(this, 'soft'), ) From 7d754ab9a80dffef2745be3bc395bff959e5b745 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 16:05:10 +0900 Subject: [PATCH 31/32] test: update --- test/snapshots/test/soft.test.ts | 50 ++++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/test/snapshots/test/soft.test.ts b/test/snapshots/test/soft.test.ts index 1b06ad825094..8692ab4fad3a 100644 --- a/test/snapshots/test/soft.test.ts +++ b/test/snapshots/test/soft.test.ts @@ -92,13 +92,13 @@ test('soft', async () => { Expected: "--file-1--" Received: "--file-1-edit--" - ❯ basic.test.ts:13:5 - 11| // console.log("[e]", e) - 12| // } - 13| await expect.soft('--file-1-edit--').toMatchFileSnapshot('./__snap… - | ^ - 14| - 15| await expect.soft('--file-2-edit--').toMatchFileSnapshot('./__snapsh… + ❯ basic.test.ts:9:3 + 7| + 8| test('toMatchFileSnapshot', async () => { + 9| await expect.soft('--file-1-edit--').toMatchFileSnapshot('./__snapsh… + | ^ + 10| await expect.soft('--file-2-edit--').toMatchFileSnapshot('./__snapsh… + 11| }) ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/6]⎯ @@ -108,13 +108,13 @@ test('soft', async () => { Expected: "--file-2--" Received: "--file-2-edit--" - ❯ basic.test.ts:15:3 - 13| await expect.soft('--file-1-edit--').toMatchFileSnapshot('./__snap… - 14| - 15| await expect.soft('--file-2-edit--').toMatchFileSnapshot('./__snapsh… + ❯ basic.test.ts:10:3 + 8| test('toMatchFileSnapshot', async () => { + 9| await expect.soft('--file-1-edit--').toMatchFileSnapshot('./__snapsh… + 10| await expect.soft('--file-2-edit--').toMatchFileSnapshot('./__snapsh… | ^ - 16| }) - 17| + 11| }) + 12| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/6]⎯ @@ -124,13 +124,13 @@ test('soft', async () => { Expected: "[Error: --error-1--]" Received: "[Error: --error-1-edit--]" - ❯ basic.test.ts:19:62 - 17| - 18| test('toThrowErrorMatchingSnapshot', () => { - 19| expect.soft(() => { throw new Error('--error-1-edit--') }).toThrowEr… + ❯ basic.test.ts:14:62 + 12| + 13| test('toThrowErrorMatchingSnapshot', () => { + 14| expect.soft(() => { throw new Error('--error-1-edit--') }).toThrowEr… | ^ - 20| expect.soft(() => { throw new Error('--error-2-edit--') }).toThrowEr… - 21| }) + 15| expect.soft(() => { throw new Error('--error-2-edit--') }).toThrowEr… + 16| }) ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/6]⎯ @@ -140,13 +140,13 @@ test('soft', async () => { Expected: "[Error: --error-2--]" Received: "[Error: --error-2-edit--]" - ❯ basic.test.ts:20:62 - 18| test('toThrowErrorMatchingSnapshot', () => { - 19| expect.soft(() => { throw new Error('--error-1-edit--') }).toThrowEr… - 20| expect.soft(() => { throw new Error('--error-2-edit--') }).toThrowEr… + ❯ basic.test.ts:15:62 + 13| test('toThrowErrorMatchingSnapshot', () => { + 14| expect.soft(() => { throw new Error('--error-1-edit--') }).toThrowEr… + 15| expect.soft(() => { throw new Error('--error-2-edit--') }).toThrowEr… | ^ - 21| }) - 22| + 16| }) + 17| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/6]⎯ From 658a1a1811ccc35ad20ae8a45170d6828db81010 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Feb 2026 16:57:06 +0900 Subject: [PATCH 32/32] refactor: no wrapAssertion for toMatchFileSnapshot --- packages/expect/src/utils.ts | 8 ++------ packages/vitest/src/integrations/snapshot/chai.ts | 6 +++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index a987856f180f..db2b1e389f96 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -41,7 +41,8 @@ export function recordAsyncExpect( if (!test.promises) { test.promises = [] } - // setup `expect.soft` handler here instead of `wrapAssertion` to avoid double tracking + // setup `expect.soft` handler here instead of `wrapAssertion` + // to avoid double error tracking while keeping non-await promise detection. if (isSoft) { promise = promise.then(noop, (err) => { handleTestError(test, err) @@ -76,7 +77,6 @@ export function recordAsyncExpect( return promise.finally(onFinally) }, [Symbol.toStringTag]: 'Promise', - ['__vitest_async_soft__' as any]: isSoft, } satisfies Promise } @@ -122,10 +122,6 @@ export function wrapAssertion( const result = fn.apply(this, args) if (result && typeof result === 'object' && typeof result.then === 'function') { - // don't invoke `then` on `expect.soft` to detect non-awaited promises - if ((result as any).__vitest_async_soft__) { - return result - } return result.then(noop, (err) => { handleTestError(test, err) }) diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 8bb65d02cdfa..9bbc1f1bcfc8 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -1,4 +1,4 @@ -import type { ChaiPlugin } from '@vitest/expect' +import type { Assertion, ChaiPlugin } from '@vitest/expect' import type { Test } from '@vitest/runner' import { equals, iterableEquality, subsetEquality } from '@vitest/expect' import { getNames } from '@vitest/runner/utils' @@ -97,7 +97,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { utils.addMethod( chai.Assertion.prototype, 'toMatchFileSnapshot', - wrapAssertion(utils, 'toMatchFileSnapshot', function (this, file: string, message?: string) { + function (this: Assertion, file: string, message?: string) { utils.flag(this, '_name', 'toMatchFileSnapshot') const isNot = utils.flag(this, 'negate') if (isNot) { @@ -126,7 +126,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { error, utils.flag(this, 'soft'), ) - }), + }, ) utils.addMethod(