Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: apply serializer to Error instance for thrown snapshot #4396

4 changes: 0 additions & 4 deletions docs/api/expect.md
Original file line number Diff line number Diff line change
Expand Up @@ -748,16 +748,12 @@ If the value in the error message is too truncated, you can increase [chaiConfig

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
Original file line number Diff line number Diff line change
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"`)
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved

//
// 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading