Skip to content

Commit

Permalink
fix: apply serializer to Error instance for thrown snapshot (#4396)
Browse files Browse the repository at this point in the history
Co-authored-by: Vladimir <sleuths.slews0s@icloud.com>
  • Loading branch information
hi-ogawa and sheremet-va committed Nov 2, 2023
1 parent 9fe3873 commit ac30972
Show file tree
Hide file tree
Showing 13 changed files with 148 additions and 44 deletions.
4 changes: 0 additions & 4 deletions docs/api/expect.md
Expand Up @@ -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<void>`
Expand Down
29 changes: 28 additions & 1 deletion docs/guide/snapshot.md
Expand Up @@ -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]`)
})
```
2 changes: 1 addition & 1 deletion examples/mocks/test/error-mock.spec.ts
Expand Up @@ -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]`)
})
17 changes: 5 additions & 12 deletions packages/vitest/src/integrations/snapshot/chai.ts
Expand Up @@ -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')
Expand Down Expand Up @@ -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),
Expand All @@ -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,
Expand Down
16 changes: 8 additions & 8 deletions 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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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
"
]
`;
4 changes: 2 additions & 2 deletions test/core/test/__snapshots__/snapshot.test.ts.snap
Expand Up @@ -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"`;
Expand All @@ -70,7 +70,7 @@ exports[`throwing 3`] = `
}
`;
exports[`throwing 4`] = `"omega"`;
exports[`throwing 4`] = `[Error: omega]`;
exports[`with big array 1`] = `
{
Expand Down
10 changes: 5 additions & 5 deletions test/core/test/jest-expect.test.ts
Expand Up @@ -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')]))
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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]`)
})
})
})
Expand Down
10 changes: 5 additions & 5 deletions test/core/test/nested-test.test.ts
Expand Up @@ -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]`)
})
})
88 changes: 88 additions & 0 deletions 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"`)
})
4 changes: 2 additions & 2 deletions test/core/test/snapshot-inline.test.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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 () => {
Expand Down
4 changes: 2 additions & 2 deletions test/core/test/wait.test.ts
Expand Up @@ -24,7 +24,7 @@ describe('waitFor', () => {
timeout: 60,
interval: 30,
}),
).rejects.toThrowErrorMatchingInlineSnapshot('"interval error"')
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: interval error]`)

expect(callback).toHaveBeenCalledTimes(2)
})
Expand Down Expand Up @@ -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)
})
Expand Down
2 changes: 1 addition & 1 deletion test/snapshots/test/snapshots-async.test.ts
Expand Up @@ -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]`)
})
2 changes: 1 addition & 1 deletion test/utils/test/display.spec.ts
Expand Up @@ -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(
Expand Down

0 comments on commit ac30972

Please sign in to comment.