From ac3097267454905e918f9a334d7d80b90b50741e Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 2 Nov 2023 23:11:35 +0900 Subject: [PATCH] fix: apply serializer to `Error` instance for thrown snapshot (#4396) Co-authored-by: Vladimir --- docs/api/expect.md | 4 - docs/guide/snapshot.md | 29 +++++- examples/mocks/test/error-mock.spec.ts | 2 +- .../vitest/src/integrations/snapshot/chai.ts | 17 ++-- .../test/__snapshots__/mocked.test.ts.snap | 16 ++-- .../test/__snapshots__/snapshot.test.ts.snap | 4 +- test/core/test/jest-expect.test.ts | 10 +-- test/core/test/nested-test.test.ts | 10 +-- .../test/snapshot-custom-serializer.test.ts | 88 +++++++++++++++++++ test/core/test/snapshot-inline.test.ts | 4 +- test/core/test/wait.test.ts | 4 +- test/snapshots/test/snapshots-async.test.ts | 2 +- test/utils/test/display.spec.ts | 2 +- 13 files changed, 148 insertions(+), 44 deletions(-) create mode 100644 test/core/test/snapshot-custom-serializer.test.ts diff --git a/docs/api/expect.md b/docs/api/expect.md index 0fee5b78f5d6..f5322a8e52cd 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -748,16 +748,12 @@ Note that since file system operation is async, you need to use `await` with `to The same as [`toMatchSnapshot`](#tomatchsnapshot), but expects the same value as [`toThrowError`](#tothrowerror). -If the function throws an `Error`, the snapshot will be the error message. Otherwise, snapshot will be the value thrown by the function. - ## toThrowErrorMatchingInlineSnapshot - **Type:** `(snapshot?: string, message?: string) => void` The same as [`toMatchInlineSnapshot`](#tomatchinlinesnapshot), but expects the same value as [`toThrowError`](#tothrowerror). -If the function throws an `Error`, the snapshot will be the error message. Otherwise, snapshot will be the value thrown by the function. - ## toHaveBeenCalled - **Type:** `() => Awaitable` diff --git a/docs/guide/snapshot.md b/docs/guide/snapshot.md index aaa9be0b173c..85f52bb649b4 100644 --- a/docs/guide/snapshot.md +++ b/docs/guide/snapshot.md @@ -237,5 +237,32 @@ exports[`toThrowErrorMatchingSnapshot: hint 1`] = `"error"`; In Vitest, the equivalent snapshot will be: ```console -exports[`toThrowErrorMatchingSnapshot > hint 1`] = `"error"`; +exports[`toThrowErrorMatchingSnapshot > hint 1`] = `[Error: error]`; +``` + +#### 4. default `Error` snapshot is different for `toThrowErrorMatchingSnapshot` and `toThrowErrorMatchingInlineSnapshot` + +```js +test('snapshot', () => { + // + // in Jest + // + + expect(new Error('error')).toMatchInlineSnapshot(`[Error: error]`) + + // Jest snapshots `Error.message` for `Error` instance + expect(() => { + throw new Error('error') + }).toThrowErrorMatchingInlineSnapshot(`"error"`) + + // + // in Vitest + // + + expect(new Error('error')).toMatchInlineSnapshot(`[Error: error]`) + + expect(() => { + throw new Error('error') + }).toThrowErrorMatchingInlineSnapshot(`[Error: error]`) +}) ``` diff --git a/examples/mocks/test/error-mock.spec.ts b/examples/mocks/test/error-mock.spec.ts index d3be7092c524..4383147d208d 100644 --- a/examples/mocks/test/error-mock.spec.ts +++ b/examples/mocks/test/error-mock.spec.ts @@ -4,5 +4,5 @@ vi.mock('../src/default', () => { test('when using top level variable, gives helpful message', async () => { await expect(() => import('../src/default').then(m => m.default)).rejects - .toThrowErrorMatchingInlineSnapshot('"[vitest] There was an error when mocking a module. If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. Read more: https://vitest.dev/api/vi.html#vi-mock"') + .toThrowErrorMatchingInlineSnapshot(`[Error: [vitest] There was an error when mocking a module. If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. Read more: https://vitest.dev/api/vi.html#vi-mock]`) }) diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 76d18b4c5847..4f86d0b0668f 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -18,27 +18,20 @@ export function getSnapshotClient(): SnapshotClient { return _client } -function getErrorMessage(err: unknown) { - if (err instanceof Error) - return err.message - - return err -} - -function getErrorString(expected: () => void | Error, promise: string | undefined) { +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}`) // when "promised", it receives thrown error - return getErrorMessage(expected) + return expected } try { expected() } catch (e) { - return getErrorMessage(e) + return e } throw new Error('snapshot function didn\'t throw') @@ -141,7 +134,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { const promise = utils.flag(this, 'promise') as string | undefined const errorMessage = utils.flag(this, 'message') getSnapshotClient().assert({ - received: getErrorString(expected, promise), + received: getError(expected, promise), message, errorMessage, ...getTestNames(test), @@ -162,7 +155,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { const errorMessage = utils.flag(this, 'message') getSnapshotClient().assert({ - received: getErrorString(expected, promise), + received: getError(expected, promise), message, inlineSnapshot, isInline: true, diff --git a/test/core/test/__snapshots__/mocked.test.ts.snap b/test/core/test/__snapshots__/mocked.test.ts.snap index 8d62b2f3967b..3662a734f533 100644 --- a/test/core/test/__snapshots__/mocked.test.ts.snap +++ b/test/core/test/__snapshots__/mocked.test.ts.snap @@ -1,7 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`mocked function which fails on toReturnWith > just one call 1`] = ` -"expected "spy" to return with: 2 at least once +[AssertionError: expected "spy" to return with: 2 at least once Received: @@ -12,11 +12,11 @@ Received: Number of calls: 1 -" +] `; exports[`mocked function which fails on toReturnWith > multi calls 1`] = ` -"expected "spy" to return with: 2 at least once +[AssertionError: expected "spy" to return with: 2 at least once Received: @@ -37,11 +37,11 @@ Received: Number of calls: 3 -" +] `; exports[`mocked function which fails on toReturnWith > oject type 1`] = ` -"expected "spy" to return with: { a: '4' } at least once +[AssertionError: expected "spy" to return with: { a: '4' } at least once Received: @@ -68,16 +68,16 @@ Received: Number of calls: 3 -" +] `; exports[`mocked function which fails on toReturnWith > zero call 1`] = ` -"expected "spy" to return with: 2 at least once +[AssertionError: expected "spy" to return with: 2 at least once Received: Number of calls: 0 -" +] `; diff --git a/test/core/test/__snapshots__/snapshot.test.ts.snap b/test/core/test/__snapshots__/snapshot.test.ts.snap index 75594ef64a1e..f3d33dccdfa1 100644 --- a/test/core/test/__snapshots__/snapshot.test.ts.snap +++ b/test/core/test/__snapshots__/snapshot.test.ts.snap @@ -60,7 +60,7 @@ exports[`single line snapshot 6`] = `"some string'"`; exports[`single line snapshot 7`] = `"some 'string'"`; -exports[`throwing 1`] = `"omega"`; +exports[`throwing 1`] = `[Error: omega]`; exports[`throwing 2`] = `"omega"`; @@ -70,7 +70,7 @@ exports[`throwing 3`] = ` } `; -exports[`throwing 4`] = `"omega"`; +exports[`throwing 4`] = `[Error: omega]`; exports[`with big array 1`] = ` { diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts index 9387910cc0b9..086c765960be 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -167,7 +167,7 @@ describe('jest-expect', () => { }).toEqual({ sum: expect.closeTo(0.4), }) - }).toThrowErrorMatchingInlineSnapshot(`"expected { sum: 0.30000000000000004 } to deeply equal { sum: CloseTo{ …(4) } }"`) + }).toThrowErrorMatchingInlineSnapshot(`[AssertionError: expected { sum: 0.30000000000000004 } to deeply equal { sum: CloseTo{ …(4) } }]`) // TODO: support set // expect(new Set(['bar'])).not.toEqual(new Set([expect.stringContaining('zoo')])) @@ -302,14 +302,14 @@ describe('jest-expect', () => { expect(() => { expect(complex).toHaveProperty('a-b', false) - }).toThrowErrorMatchingInlineSnapshot('"expected { \'0\': \'zero\', foo: 1, …(4) } to have property "a-b" with value false"') + }).toThrowErrorMatchingInlineSnapshot(`[AssertionError: expected { '0': 'zero', foo: 1, …(4) } to have property "a-b" with value false]`) expect(() => { const x = { a: { b: { c: 1 } } } const y = { a: { b: { c: 2 } } } Object.freeze(x.a) expect(x).toEqual(y) - }).toThrowErrorMatchingInlineSnapshot(`"expected { a: { b: { c: 1 } } } to deeply equal { a: { b: { c: 2 } } }"`) + }).toThrowErrorMatchingInlineSnapshot(`[AssertionError: expected { a: { b: { c: 1 } } } to deeply equal { a: { b: { c: 2 } } }]`) }) it('assertions', () => { @@ -406,14 +406,14 @@ describe('jest-expect', () => { expect(() => { expect(() => { }).toThrow(Error) - }).toThrowErrorMatchingInlineSnapshot('"expected function to throw an error, but it didn\'t"') + }).toThrowErrorMatchingInlineSnapshot(`[AssertionError: expected function to throw an error, but it didn't]`) }) it('async wasn\'t awaited', () => { expect(() => { expect(async () => { }).toThrow(Error) - }).toThrowErrorMatchingInlineSnapshot('"expected function to throw an error, but it didn\'t"') + }).toThrowErrorMatchingInlineSnapshot(`[AssertionError: expected function to throw an error, but it didn't]`) }) }) }) diff --git a/test/core/test/nested-test.test.ts b/test/core/test/nested-test.test.ts index 54efdb671258..23cf7d38d29a 100644 --- a/test/core/test/nested-test.test.ts +++ b/test/core/test/nested-test.test.ts @@ -3,28 +3,28 @@ import { describe, expect, test } from 'vitest' test('nested test should throw error', () => { expect(() => { test('test inside test', () => {}) - }).toThrowErrorMatchingInlineSnapshot(`"Nested tests are not allowed"`) + }).toThrowErrorMatchingInlineSnapshot(`[Error: Nested tests are not allowed]`) expect(() => { test.each([1, 2, 3])('test.each inside test %d', () => {}) - }).toThrowErrorMatchingInlineSnapshot(`"Nested tests are not allowed"`) + }).toThrowErrorMatchingInlineSnapshot(`[Error: Nested tests are not allowed]`) expect(() => { test.skipIf(false)('test.skipIf inside test', () => {}) - }).toThrowErrorMatchingInlineSnapshot(`"Nested tests are not allowed"`) + }).toThrowErrorMatchingInlineSnapshot(`[Error: Nested tests are not allowed]`) }) describe('parallel tests', () => { test.concurrent('parallel test 1 with nested test', () => { expect(() => { test('test inside test', () => {}) - }).toThrowErrorMatchingInlineSnapshot(`"Nested tests are not allowed"`) + }).toThrowErrorMatchingInlineSnapshot(`[Error: Nested tests are not allowed]`) }) test.concurrent('parallel test 2 without nested test', () => {}) test.concurrent('parallel test 3 without nested test', () => {}) test.concurrent('parallel test 4 with nested test', () => { expect(() => { test('test inside test', () => {}) - }).toThrowErrorMatchingInlineSnapshot(`"Nested tests are not allowed"`) + }).toThrowErrorMatchingInlineSnapshot(`[Error: Nested tests are not allowed]`) }) }) diff --git a/test/core/test/snapshot-custom-serializer.test.ts b/test/core/test/snapshot-custom-serializer.test.ts new file mode 100644 index 000000000000..a3e337361bd3 --- /dev/null +++ b/test/core/test/snapshot-custom-serializer.test.ts @@ -0,0 +1,88 @@ +import { expect, test } from 'vitest' + +test('basic', () => { + // example from docs/guide/snapshot.md + + const bar = { + foo: { + x: 1, + y: 2, + }, + } + + // without custom serializer + expect(bar).toMatchInlineSnapshot(` + { + "foo": { + "x": 1, + "y": 2, + }, + } + `) + + // with custom serializer + expect.addSnapshotSerializer({ + serialize(val, config, indentation, depth, refs, printer) { + return `Pretty foo: ${printer( + val.foo, + config, + indentation, + depth, + refs, + )}` + }, + test(val) { + return val && Object.prototype.hasOwnProperty.call(val, 'foo') + }, + }) + + expect(bar).toMatchInlineSnapshot(` + Pretty foo: { + "x": 1, + "y": 2, + } + `) +}) + +test('throwning snapshot', () => { + // example from https://github.com/vitest-dev/vitest/issues/3655 + + class ErrorWithDetails extends Error { + readonly details: unknown + + constructor(message: string, options: ErrorOptions & { details: unknown }) { + super(message, options) + this.details = options.details + } + } + + // without custom serializer + const error = new ErrorWithDetails('some-error', { + details: 'some-detail', + }) + expect(error).toMatchInlineSnapshot(`[Error: some-error]`) + expect(() => { + throw error + }).toThrowErrorMatchingInlineSnapshot(`[Error: some-error]`) + + // with custom serializer + expect.addSnapshotSerializer({ + serialize(val, config, indentation, depth, refs, printer) { + const error = val as ErrorWithDetails + return `Pretty ${error.message}: ${printer( + error.details, + config, + indentation, + depth, + refs, + )}` + }, + test(val) { + return val && val instanceof ErrorWithDetails + }, + }) + expect(error).toMatchInlineSnapshot(`Pretty some-error: "some-detail"`) + expect(() => { + throw error + }).toThrowErrorMatchingInlineSnapshot(`Pretty some-error: "some-detail"`) +}) diff --git a/test/core/test/snapshot-inline.test.ts b/test/core/test/snapshot-inline.test.ts index 4a8bbb22fc2a..657e7a243940 100644 --- a/test/core/test/snapshot-inline.test.ts +++ b/test/core/test/snapshot-inline.test.ts @@ -62,7 +62,7 @@ test('template literal', () => { test('throwing inline snapshots', async () => { expect(() => { throw new Error('omega') - }).toThrowErrorMatchingInlineSnapshot('"omega"') + }).toThrowErrorMatchingInlineSnapshot(`[Error: omega]`) expect(() => { // eslint-disable-next-line no-throw-literal @@ -102,7 +102,7 @@ test('throwing inline snapshots', async () => { await expect(async () => { throw new Error('omega') - }).rejects.toThrowErrorMatchingInlineSnapshot('"omega"') + }).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: omega]`) }) test('throwing expect should be a function', async () => { diff --git a/test/core/test/wait.test.ts b/test/core/test/wait.test.ts index a70d20037f80..3280e0408da7 100644 --- a/test/core/test/wait.test.ts +++ b/test/core/test/wait.test.ts @@ -24,7 +24,7 @@ describe('waitFor', () => { timeout: 60, interval: 30, }), - ).rejects.toThrowErrorMatchingInlineSnapshot('"interval error"') + ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: interval error]`) expect(callback).toHaveBeenCalledTimes(2) }) @@ -125,7 +125,7 @@ describe('waitUntil', () => { timeout: 60, interval: 30, }), - ).rejects.toThrowErrorMatchingInlineSnapshot('"Timed out in waitUntil!"') + ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Timed out in waitUntil!]`) expect(callback).toHaveBeenCalledTimes(2) }) diff --git a/test/snapshots/test/snapshots-async.test.ts b/test/snapshots/test/snapshots-async.test.ts index 9b61651c5e40..d176fcec317c 100644 --- a/test/snapshots/test/snapshots-async.test.ts +++ b/test/snapshots/test/snapshots-async.test.ts @@ -13,5 +13,5 @@ test('resolved inline', async () => { test('rejected inline', async () => { await expect(reject()).rejects.toMatchInlineSnapshot('[Error: foo]') - await expect(reject()).rejects.toThrowErrorMatchingInlineSnapshot('"foo"') + await expect(reject()).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: foo]`) }) diff --git a/test/utils/test/display.spec.ts b/test/utils/test/display.spec.ts index 3e304fca1f94..92b1e6eeabe2 100644 --- a/test/utils/test/display.spec.ts +++ b/test/utils/test/display.spec.ts @@ -61,7 +61,7 @@ describe('format', () => { }) test('cannont serialize some values', () => { - expect(() => format('%j', 100n)).toThrowErrorMatchingInlineSnapshot('"Do not know how to serialize a BigInt"') + expect(() => format('%j', 100n)).toThrowErrorMatchingInlineSnapshot(`[TypeError: Do not know how to serialize a BigInt]`) }) test.each(