From 509fcd991da8e0aa6de2f7f498a3db0ef2f6d182 Mon Sep 17 00:00:00 2001 From: Han Feng Date: Sun, 25 Feb 2024 15:24:36 +0800 Subject: [PATCH 1/5] fix(snapshot): `toThrowErrorMatchingInlineSnapshot` with `not` --- packages/snapshot/src/client.ts | 10 +++++++++- packages/snapshot/src/port/state.ts | 5 ++++- packages/snapshot/src/types/index.ts | 1 + packages/vitest/src/integrations/snapshot/chai.ts | 12 +++++++++--- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/snapshot/src/client.ts b/packages/snapshot/src/client.ts index 1742a810fd67..f25e0c7bc3e2 100644 --- a/packages/snapshot/src/client.ts +++ b/packages/snapshot/src/client.ts @@ -1,3 +1,4 @@ +import { AssertionError } from 'node:assert' import { deepMergeSnapshot } from './port/utils' import SnapshotState from './port/state' import type { SnapshotStateOptions } from './types' @@ -33,6 +34,7 @@ interface AssertOptions { name?: string message?: string isInline?: boolean + isNot?: boolean properties?: object inlineSnapshot?: string error?: Error @@ -96,6 +98,7 @@ export class SnapshotClient { error, errorMessage, rawSnapshot, + isNot, } = options let { received } = options @@ -131,13 +134,18 @@ export class SnapshotClient { testName, received, isInline, + isNot, error, inlineSnapshot, rawSnapshot, }) - if (!pass) + if (!pass) { + if (isNot) + throw new AssertionError({ message: `Expected not to match snapshot` }) + throw createMismatchError(`Snapshot \`${key || 'unknown'}\` mismatched`, this.snapshotState?.expand, actual?.trim(), expected?.trim()) + } } async assertRaw(options: AssertOptions): Promise { diff --git a/packages/snapshot/src/port/state.ts b/packages/snapshot/src/port/state.ts index b54139b53fc9..be5af8bb92b8 100644 --- a/packages/snapshot/src/port/state.ts +++ b/packages/snapshot/src/port/state.ts @@ -224,6 +224,7 @@ export default class SnapshotState { key, inlineSnapshot, isInline, + isNot, error, rawSnapshot, }: SnapshotMatchOptions): SnapshotReturnOptions { @@ -258,7 +259,9 @@ export default class SnapshotState { ? rawSnapshot.content : this._snapshotData[key] const expectedTrimmed = prepareExpected(expected) - const pass = expectedTrimmed === prepareExpected(receivedSerialized) + let pass = expectedTrimmed === prepareExpected(receivedSerialized) + if (isNot) + pass = !pass const hasSnapshot = expected !== undefined const snapshotIsPersisted = isInline || this._fileExists || (rawSnapshot && rawSnapshot.content != null) diff --git a/packages/snapshot/src/types/index.ts b/packages/snapshot/src/types/index.ts index 66e7baf98ed1..1c3ab17ddc1f 100644 --- a/packages/snapshot/src/types/index.ts +++ b/packages/snapshot/src/types/index.ts @@ -25,6 +25,7 @@ export interface SnapshotMatchOptions { isInline: boolean error?: Error rawSnapshot?: RawSnapshotInfo + isNot?: boolean } export interface SnapshotResult { diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 6ee0b0e62d6e..b66a9fab0456 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -18,7 +18,7 @@ export function getSnapshotClient(): SnapshotClient { return _client } -function getError(expected: () => void | Error, promise: string | undefined) { +function getError(expected: () => void | Error, promise: string | undefined, isNot: boolean = false) { if (typeof expected !== 'function') { if (!promise) throw new Error(`expected must be a function, received ${typeof expected}`) @@ -34,7 +34,8 @@ function getError(expected: () => void | Error, promise: string | undefined) { return e } - throw new Error('snapshot function didn\'t throw') + if (!isNot) + throw new Error('snapshot function didn\'t throw') } export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { @@ -153,15 +154,20 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { const error = utils.flag(this, 'error') const promise = utils.flag(this, 'promise') as string | undefined const errorMessage = utils.flag(this, 'message') + const isNot = utils.flag(this, 'negate') + const received = getError(expected, promise, isNot) + if (isNot && !(received instanceof Error)) + return if (inlineSnapshot) inlineSnapshot = stripSnapshotIndentation(inlineSnapshot) getSnapshotClient().assert({ - received: getError(expected, promise), + received, message, inlineSnapshot, isInline: true, + isNot, error, errorMessage, ...getTestNames(test), From 6dbc97e4124c8952e5e7d5b6670676346535b16b Mon Sep 17 00:00:00 2001 From: Han Feng Date: Sun, 25 Feb 2024 15:24:56 +0800 Subject: [PATCH 2/5] test: add tests --- test/core/test/snapshot-inline.test.ts | 7 +++++++ test/fails/fixtures/snapshot-with-not.test.ts | 7 +++++++ test/fails/test/__snapshots__/runner.test.ts.snap | 2 ++ test/fails/test/runner.test.ts | 2 +- 4 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 test/fails/fixtures/snapshot-with-not.test.ts diff --git a/test/core/test/snapshot-inline.test.ts b/test/core/test/snapshot-inline.test.ts index fa91f4f74873..898d0ae73263 100644 --- a/test/core/test/snapshot-inline.test.ts +++ b/test/core/test/snapshot-inline.test.ts @@ -127,6 +127,13 @@ test('throwing expect should be a function', async () => { }).toThrow(/expected must be a function/) }) +test('throwing inline snapshots with not', async () => { + expect(() => {}).not.toThrowErrorMatchingInlineSnapshot() + expect(() => { + throw new Error('hi') + }).not.toThrowErrorMatchingInlineSnapshot(`[Error: hello]`) +}) + test('properties inline snapshot', () => { const user = { createdAt: new Date(), diff --git a/test/fails/fixtures/snapshot-with-not.test.ts b/test/fails/fixtures/snapshot-with-not.test.ts new file mode 100644 index 000000000000..2b3148d05458 --- /dev/null +++ b/test/fails/fixtures/snapshot-with-not.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from "vitest" + +test('', async () => { + expect(() => { + throw new Error('hi') + }).not.toThrowErrorMatchingInlineSnapshot(`[Error: hi]`) +}) diff --git a/test/fails/test/__snapshots__/runner.test.ts.snap b/test/fails/test/__snapshots__/runner.test.ts.snap index 5ee589c515ee..9e011f4ef73f 100644 --- a/test/fails/test/__snapshots__/runner.test.ts.snap +++ b/test/fails/test/__snapshots__/runner.test.ts.snap @@ -33,6 +33,8 @@ exports[`should fail nested-suite.test.ts > nested-suite.test.ts 1`] = `"Asserti exports[`should fail primitive-error.test.ts > primitive-error.test.ts 1`] = `"Unknown Error: 42"`; +exports[`should fail snapshot-with-not.test.ts > snapshot-with-not.test.ts 1`] = `"AssertionError: Expected not to match snapshot"`; + exports[`should fail stall.test.ts > stall.test.ts 1`] = ` "TypeError: failure TypeError: failure diff --git a/test/fails/test/runner.test.ts b/test/fails/test/runner.test.ts index 6486ae9416f9..c5017e0d542b 100644 --- a/test/fails/test/runner.test.ts +++ b/test/fails/test/runner.test.ts @@ -14,7 +14,7 @@ it.each(files)('should fail %s', async (file) => { const msg = String(stderr) .split(/\n/g) .reverse() - .filter(i => i.includes('Error: ') && !i.includes('Command failed') && !i.includes('stackStr') && !i.includes('at runTest')) + .filter(i => i.includes('Error: ') && !i.includes('Command failed') && !i.includes('stackStr') && !i.includes('at runTest') && !/\d\|/.test(i)) .map(i => i.trim().replace(root, ''), ).join('\n') expect(msg).toMatchSnapshot(file) From 681a0fd399777f89b8c17327d856d8937474133c Mon Sep 17 00:00:00 2001 From: Han Feng Date: Sun, 25 Feb 2024 15:44:08 +0800 Subject: [PATCH 3/5] fix: replace AssertionError with Error --- packages/snapshot/src/client.ts | 3 +-- test/fails/test/__snapshots__/runner.test.ts.snap | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/snapshot/src/client.ts b/packages/snapshot/src/client.ts index f25e0c7bc3e2..23593a56bcca 100644 --- a/packages/snapshot/src/client.ts +++ b/packages/snapshot/src/client.ts @@ -1,4 +1,3 @@ -import { AssertionError } from 'node:assert' import { deepMergeSnapshot } from './port/utils' import SnapshotState from './port/state' import type { SnapshotStateOptions } from './types' @@ -142,7 +141,7 @@ export class SnapshotClient { if (!pass) { if (isNot) - throw new AssertionError({ message: `Expected not to match snapshot` }) + throw new Error(`Expected not to match snapshot`) throw createMismatchError(`Snapshot \`${key || 'unknown'}\` mismatched`, this.snapshotState?.expand, actual?.trim(), expected?.trim()) } diff --git a/test/fails/test/__snapshots__/runner.test.ts.snap b/test/fails/test/__snapshots__/runner.test.ts.snap index 9e011f4ef73f..61a3c989d961 100644 --- a/test/fails/test/__snapshots__/runner.test.ts.snap +++ b/test/fails/test/__snapshots__/runner.test.ts.snap @@ -33,7 +33,7 @@ exports[`should fail nested-suite.test.ts > nested-suite.test.ts 1`] = `"Asserti exports[`should fail primitive-error.test.ts > primitive-error.test.ts 1`] = `"Unknown Error: 42"`; -exports[`should fail snapshot-with-not.test.ts > snapshot-with-not.test.ts 1`] = `"AssertionError: Expected not to match snapshot"`; +exports[`should fail snapshot-with-not.test.ts > snapshot-with-not.test.ts 1`] = `"Error: Expected not to match snapshot"`; exports[`should fail stall.test.ts > stall.test.ts 1`] = ` "TypeError: failure From 8f66752f887af85c792bfe15503ccdb722c9ac25 Mon Sep 17 00:00:00 2001 From: Han Feng Date: Mon, 26 Feb 2024 21:06:05 +0800 Subject: [PATCH 4/5] chore: revert and throw error if used with `not` --- packages/snapshot/src/client.ts | 8 +----- packages/snapshot/src/port/state.ts | 5 +--- packages/snapshot/src/types/index.ts | 1 - .../vitest/src/integrations/snapshot/chai.ts | 26 +++++++++++++------ test/core/test/snapshot-inline.test.ts | 7 ----- test/fails/fixtures/snapshot-with-not.test.ts | 12 ++++++--- .../test/__snapshots__/runner.test.ts.snap | 8 +++++- test/fails/test/runner.test.ts | 2 +- 8 files changed, 36 insertions(+), 33 deletions(-) diff --git a/packages/snapshot/src/client.ts b/packages/snapshot/src/client.ts index 23593a56bcca..3789f647a885 100644 --- a/packages/snapshot/src/client.ts +++ b/packages/snapshot/src/client.ts @@ -97,7 +97,6 @@ export class SnapshotClient { error, errorMessage, rawSnapshot, - isNot, } = options let { received } = options @@ -133,18 +132,13 @@ export class SnapshotClient { testName, received, isInline, - isNot, error, inlineSnapshot, rawSnapshot, }) - if (!pass) { - if (isNot) - throw new Error(`Expected not to match snapshot`) - + if (!pass) throw createMismatchError(`Snapshot \`${key || 'unknown'}\` mismatched`, this.snapshotState?.expand, actual?.trim(), expected?.trim()) - } } async assertRaw(options: AssertOptions): Promise { diff --git a/packages/snapshot/src/port/state.ts b/packages/snapshot/src/port/state.ts index be5af8bb92b8..b54139b53fc9 100644 --- a/packages/snapshot/src/port/state.ts +++ b/packages/snapshot/src/port/state.ts @@ -224,7 +224,6 @@ export default class SnapshotState { key, inlineSnapshot, isInline, - isNot, error, rawSnapshot, }: SnapshotMatchOptions): SnapshotReturnOptions { @@ -259,9 +258,7 @@ export default class SnapshotState { ? rawSnapshot.content : this._snapshotData[key] const expectedTrimmed = prepareExpected(expected) - let pass = expectedTrimmed === prepareExpected(receivedSerialized) - if (isNot) - pass = !pass + const pass = expectedTrimmed === prepareExpected(receivedSerialized) const hasSnapshot = expected !== undefined const snapshotIsPersisted = isInline || this._fileExists || (rawSnapshot && rawSnapshot.content != null) diff --git a/packages/snapshot/src/types/index.ts b/packages/snapshot/src/types/index.ts index 1c3ab17ddc1f..66e7baf98ed1 100644 --- a/packages/snapshot/src/types/index.ts +++ b/packages/snapshot/src/types/index.ts @@ -25,7 +25,6 @@ export interface SnapshotMatchOptions { isInline: boolean error?: Error rawSnapshot?: RawSnapshotInfo - isNot?: boolean } export interface SnapshotResult { diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index b66a9fab0456..437ccc88cf32 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -18,7 +18,7 @@ export function getSnapshotClient(): SnapshotClient { return _client } -function getError(expected: () => void | Error, promise: string | undefined, isNot: boolean = false) { +function getError(expected: () => void | Error, promise: string | undefined) { if (typeof expected !== 'function') { if (!promise) throw new Error(`expected must be a function, received ${typeof expected}`) @@ -34,8 +34,7 @@ function getError(expected: () => void | Error, promise: string | undefined, isN return e } - if (!isNot) - throw new Error('snapshot function didn\'t throw') + throw new Error('snapshot function didn\'t throw') } export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { @@ -53,6 +52,9 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { chai.Assertion.prototype, key, function (this: Record, properties?: object, message?: string) { + 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 = utils.flag(this, 'vitest-test') if (typeof properties === 'string' && typeof message === 'undefined') { @@ -76,6 +78,9 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { chai.Assertion.prototype, 'toMatchFileSnapshot', function (this: Record, file: string, message?: string) { + const isNot = utils.flag(this, 'negate') + if (isNot) + throw new Error('toMatchFileSnapshot cannot be used with "not"') const expected = utils.flag(this, 'object') const test = utils.flag(this, 'vitest-test') as Test const errorMessage = utils.flag(this, 'message') @@ -99,6 +104,9 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { chai.Assertion.prototype, 'toMatchInlineSnapshot', function __INLINE_SNAPSHOT__(this: Record, properties?: object, inlineSnapshot?: string, message?: string) { + const isNot = utils.flag(this, 'negate') + if (isNot) + throw new Error('toMatchInlineSnapshot cannot be used with "not"') const test = utils.flag(this, 'vitest-test') const isInsideEach = test && (test.each || test.suite?.each) if (isInsideEach) @@ -130,6 +138,9 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { chai.Assertion.prototype, 'toThrowErrorMatchingSnapshot', function (this: Record, message?: string) { + const isNot = utils.flag(this, 'negate') + if (isNot) + throw new Error('toThrowErrorMatchingSnapshot cannot be used with "not"') const expected = utils.flag(this, 'object') const test = utils.flag(this, 'vitest-test') const promise = utils.flag(this, 'promise') as string | undefined @@ -146,6 +157,9 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { chai.Assertion.prototype, 'toThrowErrorMatchingInlineSnapshot', function __INLINE_SNAPSHOT__(this: Record, inlineSnapshot: string, message: string) { + const isNot = utils.flag(this, 'negate') + if (isNot) + throw new Error('toThrowErrorMatchingInlineSnapshot cannot be used with "not"') const test = utils.flag(this, 'vitest-test') const isInsideEach = test && (test.each || test.suite?.each) if (isInsideEach) @@ -154,10 +168,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { const error = utils.flag(this, 'error') const promise = utils.flag(this, 'promise') as string | undefined const errorMessage = utils.flag(this, 'message') - const isNot = utils.flag(this, 'negate') - const received = getError(expected, promise, isNot) - if (isNot && !(received instanceof Error)) - return + const received = getError(expected, promise) if (inlineSnapshot) inlineSnapshot = stripSnapshotIndentation(inlineSnapshot) @@ -167,7 +178,6 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { message, inlineSnapshot, isInline: true, - isNot, error, errorMessage, ...getTestNames(test), diff --git a/test/core/test/snapshot-inline.test.ts b/test/core/test/snapshot-inline.test.ts index 898d0ae73263..fa91f4f74873 100644 --- a/test/core/test/snapshot-inline.test.ts +++ b/test/core/test/snapshot-inline.test.ts @@ -127,13 +127,6 @@ test('throwing expect should be a function', async () => { }).toThrow(/expected must be a function/) }) -test('throwing inline snapshots with not', async () => { - expect(() => {}).not.toThrowErrorMatchingInlineSnapshot() - expect(() => { - throw new Error('hi') - }).not.toThrowErrorMatchingInlineSnapshot(`[Error: hello]`) -}) - test('properties inline snapshot', () => { const user = { createdAt: new Date(), diff --git a/test/fails/fixtures/snapshot-with-not.test.ts b/test/fails/fixtures/snapshot-with-not.test.ts index 2b3148d05458..f4de629e2b50 100644 --- a/test/fails/fixtures/snapshot-with-not.test.ts +++ b/test/fails/fixtures/snapshot-with-not.test.ts @@ -1,7 +1,11 @@ import { expect, test } from "vitest" -test('', async () => { - expect(() => { - throw new Error('hi') - }).not.toThrowErrorMatchingInlineSnapshot(`[Error: hi]`) +test.each([ + 'toMatchSnapshot', + 'toMatchFileSnapshot', + 'toMatchInlineSnapshot', + 'toThrowErrorMatchingSnapshot', + 'toThrowErrorMatchingInlineSnapshot', +])('%s should fail with not', (api) => { + (expect(0).not as any)[api]() }) diff --git a/test/fails/test/__snapshots__/runner.test.ts.snap b/test/fails/test/__snapshots__/runner.test.ts.snap index 61a3c989d961..6fe18ff23435 100644 --- a/test/fails/test/__snapshots__/runner.test.ts.snap +++ b/test/fails/test/__snapshots__/runner.test.ts.snap @@ -33,7 +33,13 @@ exports[`should fail nested-suite.test.ts > nested-suite.test.ts 1`] = `"Asserti exports[`should fail primitive-error.test.ts > primitive-error.test.ts 1`] = `"Unknown Error: 42"`; -exports[`should fail snapshot-with-not.test.ts > snapshot-with-not.test.ts 1`] = `"Error: Expected not to match snapshot"`; +exports[`should fail snapshot-with-not.test.ts > snapshot-with-not.test.ts 1`] = ` +"Error: toThrowErrorMatchingInlineSnapshot cannot be used with "not" +Error: toThrowErrorMatchingSnapshot cannot be used with "not" +Error: toMatchInlineSnapshot cannot be used with "not" +Error: toMatchFileSnapshot cannot be used with "not" +Error: toMatchSnapshot cannot be used with "not"" +`; exports[`should fail stall.test.ts > stall.test.ts 1`] = ` "TypeError: failure diff --git a/test/fails/test/runner.test.ts b/test/fails/test/runner.test.ts index c5017e0d542b..6486ae9416f9 100644 --- a/test/fails/test/runner.test.ts +++ b/test/fails/test/runner.test.ts @@ -14,7 +14,7 @@ it.each(files)('should fail %s', async (file) => { const msg = String(stderr) .split(/\n/g) .reverse() - .filter(i => i.includes('Error: ') && !i.includes('Command failed') && !i.includes('stackStr') && !i.includes('at runTest') && !/\d\|/.test(i)) + .filter(i => i.includes('Error: ') && !i.includes('Command failed') && !i.includes('stackStr') && !i.includes('at runTest')) .map(i => i.trim().replace(root, ''), ).join('\n') expect(msg).toMatchSnapshot(file) From 7827392ce3a84d33268dd568ee4ca76c773f5c1b Mon Sep 17 00:00:00 2001 From: Han Feng Date: Mon, 26 Feb 2024 21:11:42 +0800 Subject: [PATCH 5/5] chore: update --- packages/snapshot/src/client.ts | 1 - packages/vitest/src/integrations/snapshot/chai.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/snapshot/src/client.ts b/packages/snapshot/src/client.ts index 3789f647a885..1742a810fd67 100644 --- a/packages/snapshot/src/client.ts +++ b/packages/snapshot/src/client.ts @@ -33,7 +33,6 @@ interface AssertOptions { name?: string message?: string isInline?: boolean - isNot?: boolean properties?: object inlineSnapshot?: string error?: Error diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 437ccc88cf32..6f9ebbd6b79c 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -168,13 +168,12 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { const error = utils.flag(this, 'error') const promise = utils.flag(this, 'promise') as string | undefined const errorMessage = utils.flag(this, 'message') - const received = getError(expected, promise) if (inlineSnapshot) inlineSnapshot = stripSnapshotIndentation(inlineSnapshot) getSnapshotClient().assert({ - received, + received: getError(expected, promise), message, inlineSnapshot, isInline: true,