diff --git a/docs/guide/extending-matchers.md b/docs/guide/extending-matchers.md
index 27653c838a90..2d634c22b049 100644
--- a/docs/guide/extending-matchers.md
+++ b/docs/guide/extending-matchers.md
@@ -107,6 +107,10 @@ function customMatcher(this: MatcherState, received: unknown, arg1: unknown, arg
expect.extend({ customMatcher })
```
+::: tip
+To build custom **snapshot matchers** (wrappers around `toMatchSnapshot` / `toMatchInlineSnapshot` / `toMatchFileSnapshot`), use the composable functions from `vitest/runtime`. See [Custom Snapshot Matchers](/guide/snapshot#custom-snapshot-matchers).
+:::
+
Matcher function has access to `this` context with the following properties:
## `isNot`
diff --git a/docs/guide/migration.md b/docs/guide/migration.md
index e5742cc8b458..952da819b200 100644
--- a/docs/guide/migration.md
+++ b/docs/guide/migration.md
@@ -650,6 +650,36 @@ export default defineConfig({
Otherwise your snapshots will have a lot of escaped `"` characters.
+### Custom Snapshot Matchers experimental 4.1.3
+
+Jest imports snapshot composables from `jest-snapshot`. In Vitest, import from `vitest/runtime` instead:
+
+```ts
+const { toMatchSnapshot } = require('jest-snapshot') // [!code --]
+import { toMatchSnapshot } from 'vitest/runtime' // [!code ++]
+
+expect.extend({
+ toMatchTrimmedSnapshot(received: string, length: number) {
+ return toMatchSnapshot.call(this, received.slice(0, length))
+ },
+})
+```
+
+For inline snapshots, the same applies:
+
+```ts
+const { toMatchInlineSnapshot } = require('jest-snapshot') // [!code --]
+import { toMatchInlineSnapshot } from 'vitest/runtime' // [!code ++]
+
+expect.extend({
+ toMatchTrimmedInlineSnapshot(received: string, inlineSnapshot?: string) {
+ return toMatchInlineSnapshot.call(this, received.slice(0, 10), inlineSnapshot)
+ },
+})
+```
+
+See [Custom Snapshot Matchers](/guide/snapshot#custom-snapshot-matchers) for the full guide.
+
## Migrating from Mocha + Chai + Sinon {#mocha-chai-sinon}
Vitest provides excellent support for migrating from Mocha+Chai+Sinon test suites. While Vitest uses a Jest-compatible API by default, it also provides Chai-style assertions for spy/mock testing, making migration easier.
diff --git a/docs/guide/snapshot.md b/docs/guide/snapshot.md
index 18b0b58a636a..2ca266c4a8c8 100644
--- a/docs/guide/snapshot.md
+++ b/docs/guide/snapshot.md
@@ -200,6 +200,76 @@ Pretty foo: Object {
We are using Jest's `pretty-format` for serializing snapshots. You can read more about it here: [pretty-format](https://github.com/facebook/jest/blob/main/packages/pretty-format/README.md#serialize).
+## Custom Snapshot Matchers experimental 4.1.3 {#custom-snapshot-matchers}
+
+You can build custom snapshot matchers using the composable functions exported from `vitest/runtime`. These let you transform values before snapshotting while preserving full snapshot lifecycle support (creation, update, inline rewriting).
+
+```ts
+import { expect, test } from 'vitest'
+import { toMatchFileSnapshot, toMatchInlineSnapshot, toMatchSnapshot } from 'vitest/runtime'
+
+expect.extend({
+ toMatchTrimmedSnapshot(received: string, length: number) {
+ return toMatchSnapshot.call(this, received.slice(0, length))
+ },
+ toMatchTrimmedInlineSnapshot(received: string, inlineSnapshot?: string) {
+ return toMatchInlineSnapshot.call(this, received.slice(0, 10), inlineSnapshot)
+ },
+ async toMatchTrimmedFileSnapshot(received: string, file: string) {
+ return toMatchFileSnapshot.call(this, received.slice(0, 10), file)
+ },
+})
+
+test('file snapshot', () => {
+ expect('extra long string oh my gerd').toMatchTrimmedSnapshot(10)
+})
+
+test('inline snapshot', () => {
+ expect('extra long string oh my gerd').toMatchTrimmedInlineSnapshot()
+})
+
+test('raw file snapshot', async () => {
+ await expect('extra long string oh my gerd').toMatchTrimmedFileSnapshot('./raw-file.txt')
+})
+```
+
+The composables return `{ pass, message }` so you can further customize the error:
+
+```ts
+expect.extend({
+ toMatchTrimmedSnapshot(received: string, length: number) {
+ const result = toMatchSnapshot.call(this, received.slice(0, length))
+ return { ...result, message: () => `Trimmed snapshot failed: ${result.message()}` }
+ },
+})
+```
+
+::: warning
+For inline snapshot matchers, the snapshot argument must be the last parameter (or second-to-last when using property matchers). Vitest rewrites the last string argument in the source code, so custom arguments before the snapshot work, but custom arguments after it are not supported.
+:::
+
+::: tip
+File snapshot matchers must be `async` — `toMatchFileSnapshot` returns a `Promise`. Remember to `await` the result in the matcher and in your test.
+:::
+
+For TypeScript, extend the `Assertion` interface:
+
+```ts
+import 'vitest'
+
+declare module 'vitest' {
+ interface Assertion {
+ toMatchTrimmedSnapshot: (length: number) => T
+ toMatchTrimmedInlineSnapshot: (inlineSnapshot?: string) => T
+ toMatchTrimmedFileSnapshot: (file: string) => Promise
+ }
+}
+```
+
+::: tip
+See [Extending Matchers](/guide/extending-matchers) for more on `expect.extend` and custom matcher conventions.
+:::
+
## Difference from Jest
Vitest provides an almost compatible Snapshot feature with [Jest's](https://jestjs.io/docs/snapshot-testing) with a few exceptions:
diff --git a/packages/expect/src/jest-extend.ts b/packages/expect/src/jest-extend.ts
index 5352523c132d..c8e19c4bfdf8 100644
--- a/packages/expect/src/jest-extend.ts
+++ b/packages/expect/src/jest-extend.ts
@@ -53,6 +53,7 @@ function getMatcherState(
suppressedErrors: [],
soft: util.flag(assertion, 'soft') as boolean | undefined,
poll: util.flag(assertion, 'poll') as boolean | undefined,
+ __vitest_assertion__: assertion as any,
}
Object.assign(matcherState, { task })
@@ -89,7 +90,7 @@ function JestExtendPlugin(
return (_, utils) => {
Object.entries(matchers).forEach(
([expectAssertionName, expectAssertion]) => {
- function expectWrapper(
+ function __VITEST_EXTEND_ASSERTION__(
this: Chai.AssertionStatic & Chai.Assertion,
...args: any[]
) {
@@ -133,7 +134,7 @@ function JestExtendPlugin(
}
}
- const softWrapper = wrapAssertion(utils, expectAssertionName, expectWrapper)
+ const softWrapper = wrapAssertion(utils, expectAssertionName, __VITEST_EXTEND_ASSERTION__)
utils.addMethod(
(globalThis as any)[JEST_MATCHERS_OBJECT].matchers,
expectAssertionName,
diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts
index eea5e1f38e54..eeaf681d3714 100644
--- a/packages/expect/src/types.ts
+++ b/packages/expect/src/types.ts
@@ -82,6 +82,13 @@ export interface MatcherState {
}
soft?: boolean
poll?: boolean
+ /**
+ * this allows `expect.extend`-based custom matcher
+ * to implement builtin vitest/chai assertion equivalent feature.
+ * this used for custom snapshot matcher API.
+ */
+ /** @internal */
+ __vitest_assertion__: Assertion
}
export interface SyncExpectationResult {
diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts
index 13b2620055da..49138dab7c2e 100644
--- a/packages/expect/src/utils.ts
+++ b/packages/expect/src/utils.ts
@@ -5,7 +5,7 @@ import { noop } from '@vitest/utils/helpers'
export function createAssertionMessage(
util: Chai.ChaiUtils,
- assertion: Assertion,
+ assertion: Chai.Assertion,
hasArgs: boolean,
) {
const soft = util.flag(assertion, 'soft') ? '.soft' : ''
@@ -92,6 +92,7 @@ function handleTestError(test: Test, err: unknown) {
test.result.errors.push(processError(err))
}
+/** wrap assertion function to support `expect.soft` and provide assertion name as `_name` */
export function wrapAssertion(
utils: Chai.ChaiUtils,
name: string,
diff --git a/packages/snapshot/src/client.ts b/packages/snapshot/src/client.ts
index 72da2a9e39c5..ca41d9e337d0 100644
--- a/packages/snapshot/src/client.ts
+++ b/packages/snapshot/src/client.ts
@@ -48,6 +48,7 @@ interface AssertOptions {
error?: Error
errorMessage?: string
rawSnapshot?: RawSnapshotInfo
+ assertionName?: string
}
/** Same shape as expect.extend custom matcher result (SyncExpectationResult from @vitest/expect) */
@@ -119,6 +120,7 @@ export class SnapshotClient {
error,
errorMessage,
rawSnapshot,
+ assertionName,
} = options
let { received } = options
@@ -173,6 +175,7 @@ export class SnapshotClient {
error,
inlineSnapshot,
rawSnapshot,
+ assertionName,
})
return {
diff --git a/packages/snapshot/src/port/inlineSnapshot.ts b/packages/snapshot/src/port/inlineSnapshot.ts
index 185080f54ba6..59d6c5116239 100644
--- a/packages/snapshot/src/port/inlineSnapshot.ts
+++ b/packages/snapshot/src/port/inlineSnapshot.ts
@@ -6,6 +6,7 @@ import {
offsetToLineNumber,
positionToOffset,
} from '@vitest/utils/offset'
+import { memo } from './utils'
export interface InlineSnapshot {
snapshot: string
@@ -13,6 +14,10 @@ export interface InlineSnapshot {
file: string
line: number
column: number
+ // it maybe possible to accurately extract this from `ParsedStack.method`,
+ // but for now, we ask higher level assertion to pass it explicitly
+ // since this is useful for certain error messages before we extract stack.
+ assertionName?: string
}
export async function saveInlineSnapshots(
@@ -33,7 +38,7 @@ export async function saveInlineSnapshots(
for (const snap of snaps) {
const index = positionToOffset(code, snap.line, snap.column)
- replaceInlineSnap(code, s, index, snap.snapshot)
+ replaceInlineSnap(code, s, index, snap.snapshot, snap.assertionName)
}
const transformed = s.toString()
@@ -44,17 +49,31 @@ export async function saveInlineSnapshots(
)
}
-const startObjectRegex
+const defaultStartObjectRegex
= /(?:toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot)\s*\(\s*(?:\/\*[\s\S]*\*\/\s*|\/\/.*(?:[\n\r\u2028\u2029]\s*|[\t\v\f \xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF]))*\{/
+function escapeRegExp(s: string): string {
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+}
+
+const buildStartObjectRegex = memo((assertionName: string) => {
+ const replaced = defaultStartObjectRegex.source.replace(
+ 'toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot',
+ escapeRegExp(assertionName),
+ )
+ return new RegExp(replaced)
+})
+
function replaceObjectSnap(
code: string,
s: MagicString,
index: number,
newSnap: string,
+ assertionName?: string,
) {
let _code = code.slice(index)
- const startMatch = startObjectRegex.exec(_code)
+ const regex = assertionName ? buildStartObjectRegex(assertionName) : defaultStartObjectRegex
+ const startMatch = regex.exec(_code)
if (!startMatch) {
return false
}
@@ -121,23 +140,17 @@ function prepareSnapString(snap: string, source: string, index: number) {
.replace(/\$\{/g, '\\${')}\n${indent}${quote}`
}
-const toMatchInlineName = 'toMatchInlineSnapshot'
-const toThrowErrorMatchingInlineName = 'toThrowErrorMatchingInlineSnapshot'
+const defaultMethodNames = ['toMatchInlineSnapshot', 'toThrowErrorMatchingInlineSnapshot']
// on webkit, the line number is at the end of the method, not at the start
-function getCodeStartingAtIndex(code: string, index: number) {
- const indexInline = index - toMatchInlineName.length
- if (code.slice(indexInline, index) === toMatchInlineName) {
- return {
- code: code.slice(indexInline),
- index: indexInline,
- }
- }
- const indexThrowInline = index - toThrowErrorMatchingInlineName.length
- if (code.slice(index - indexThrowInline, index) === toThrowErrorMatchingInlineName) {
- return {
- code: code.slice(index - indexThrowInline),
- index: index - indexThrowInline,
+function getCodeStartingAtIndex(code: string, index: number, methodNames: string[]) {
+ for (const name of methodNames) {
+ const adjusted = index - name.length
+ if (adjusted >= 0 && code.slice(adjusted, index) === name) {
+ return {
+ code: code.slice(adjusted),
+ index: adjusted,
+ }
}
}
return {
@@ -146,24 +159,35 @@ function getCodeStartingAtIndex(code: string, index: number) {
}
}
-const startRegex
+const defaultStartRegex
= /(?:toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot)\s*\(\s*(?:\/\*[\s\S]*\*\/\s*|\/\/.*(?:[\n\r\u2028\u2029]\s*|[\t\v\f \xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF]))*[\w$]*(['"`)])/
+
+const buildStartRegex = memo((assertionName: string) => {
+ const replaced = defaultStartRegex.source.replace(
+ 'toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot',
+ escapeRegExp(assertionName),
+ )
+ return new RegExp(replaced)
+})
+
export function replaceInlineSnap(
code: string,
s: MagicString,
currentIndex: number,
newSnap: string,
+ assertionName?: string,
): boolean {
- const { code: codeStartingAtIndex, index } = getCodeStartingAtIndex(code, currentIndex)
+ const methodNames = assertionName ? [assertionName] : defaultMethodNames
+ const { code: codeStartingAtIndex, index } = getCodeStartingAtIndex(code, currentIndex, methodNames)
+ const startRegex = assertionName ? buildStartRegex(assertionName) : defaultStartRegex
const startMatch = startRegex.exec(codeStartingAtIndex)
- const firstKeywordMatch = /toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot/.exec(
- codeStartingAtIndex,
- )
+ const keywordRegex = assertionName ? new RegExp(escapeRegExp(assertionName)) : /toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot/
+ const firstKeywordMatch = keywordRegex.exec(codeStartingAtIndex)
if (!startMatch || startMatch.index !== firstKeywordMatch?.index) {
- return replaceObjectSnap(code, s, index, newSnap)
+ return replaceObjectSnap(code, s, index, newSnap, assertionName)
}
const quote = startMatch[1]
diff --git a/packages/snapshot/src/port/state.ts b/packages/snapshot/src/port/state.ts
index 39e905808ac8..e2e9ddc2870a 100644
--- a/packages/snapshot/src/port/state.ts
+++ b/packages/snapshot/src/port/state.ts
@@ -177,6 +177,15 @@ export default class SnapshotState {
}
}
+ // custom matcher registered via expect.extend() — the wrapper function
+ // in jest-extend.ts is named __VITEST_EXTEND_ASSERTION__
+ const customMatcherIndex = stacks.findIndex(i =>
+ i.method.includes('__VITEST_EXTEND_ASSERTION__'),
+ )
+ if (customMatcherIndex !== -1) {
+ return stacks[customMatcherIndex + 3] ?? null
+ }
+
// inline snapshot function is called __INLINE_SNAPSHOT__
// in integrations/snapshot/chai.ts
const stackIndex = stacks.findIndex(i =>
@@ -188,14 +197,15 @@ export default class SnapshotState {
private _addSnapshot(
key: string,
receivedSerialized: string,
- options: { rawSnapshot?: RawSnapshotInfo; stack?: ParsedStack; testId: string },
+ options: { rawSnapshot?: RawSnapshotInfo; stack?: ParsedStack; testId: string; assertionName?: string },
): void {
this._dirty = true
if (options.stack) {
this._inlineSnapshots.push({
+ ...options.stack,
snapshot: receivedSerialized,
testId: options.testId,
- ...options.stack,
+ assertionName: options.assertionName,
})
}
else if (options.rawSnapshot) {
@@ -294,6 +304,7 @@ export default class SnapshotState {
isInline,
error,
rawSnapshot,
+ assertionName,
}: SnapshotMatchOptions): SnapshotReturnOptions {
// this also increments counter for inline snapshots. maybe we shouldn't?
this._counters.increment(testName)
@@ -422,6 +433,7 @@ export default class SnapshotState {
stack,
testId,
rawSnapshot,
+ assertionName,
})
}
else {
@@ -433,6 +445,7 @@ export default class SnapshotState {
stack,
testId,
rawSnapshot,
+ assertionName,
})
this.added.increment(testId)
}
diff --git a/packages/snapshot/src/port/utils.ts b/packages/snapshot/src/port/utils.ts
index b096aed842f5..9534251f9958 100644
--- a/packages/snapshot/src/port/utils.ts
+++ b/packages/snapshot/src/port/utils.ts
@@ -286,3 +286,14 @@ export class CounterMap extends DefaultMap {
return total
}
}
+
+/* @__NO_SIDE_EFFECTS__ */
+export function memo(fn: (arg: T) => U): (arg: T) => U {
+ const cache = new Map()
+ return (arg: T) => {
+ if (!cache.has(arg)) {
+ cache.set(arg, fn(arg))
+ }
+ return cache.get(arg)!
+ }
+}
diff --git a/packages/snapshot/src/types/index.ts b/packages/snapshot/src/types/index.ts
index f314d3befa15..e58d99428fa7 100644
--- a/packages/snapshot/src/types/index.ts
+++ b/packages/snapshot/src/types/index.ts
@@ -32,6 +32,7 @@ export interface SnapshotMatchOptions {
isInline: boolean
error?: Error
rawSnapshot?: RawSnapshotInfo
+ assertionName?: string
}
export interface SnapshotResult {
diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts
index 908f1061d616..67946107fc51 100644
--- a/packages/vitest/src/integrations/snapshot/chai.ts
+++ b/packages/vitest/src/integrations/snapshot/chai.ts
@@ -1,12 +1,13 @@
-import type { Assertion, ChaiPlugin } from '@vitest/expect'
+import type { ChaiPlugin, MatcherState, SyncExpectationResult } from '@vitest/expect'
import type { Test } from '@vitest/runner'
-import { createAssertionMessage, equals, iterableEquality, recordAsyncExpect, subsetEquality, wrapAssertion } from '@vitest/expect'
+import { chai, createAssertionMessage, equals, iterableEquality, recordAsyncExpect, subsetEquality, wrapAssertion } from '@vitest/expect'
import { getNames } from '@vitest/runner/utils'
import {
addSerializer,
SnapshotClient,
stripSnapshotIndentation,
} from '@vitest/snapshot'
+import { getWorkerState } from '../../runtime/utils'
let _client: SnapshotClient
@@ -51,44 +52,44 @@ function getTestNames(test: Test) {
}
}
-export const SnapshotPlugin: ChaiPlugin = (chai, utils) => {
- function getTest(assertionName: string, obj: object) {
- const test = utils.flag(obj, 'vitest-test')
- if (!test) {
- throw new Error(`'${assertionName}' cannot be used without test context`)
- }
- return test as Test
+function getAssertionName(assertion: Chai.Assertion): string {
+ const name = chai.util.flag(assertion, '_name') as string | undefined
+ if (!name) {
+ throw new Error('Assertion name is not set. This is a bug in Vitest. Please, open a new issue with reproduction.')
+ }
+ return name
+}
+
+function getTest(obj: Chai.Assertion) {
+ const test = chai.util.flag(obj, 'vitest-test')
+ if (!test) {
+ throw new Error(`'${getAssertionName(obj)}' cannot be used without test context`)
}
+ return test as Test
+}
+
+function validateAssertion(assertion: Chai.Assertion): void {
+ if (chai.util.flag(assertion, 'negate')) {
+ throw new Error(`${getAssertionName(assertion)} cannot be used with "not"`)
+ }
+}
+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,
+ propertiesOrHint?: object | string,
+ hint?: 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),
+ const result = toMatchSnapshotImpl({
+ assertion: this,
+ received: utils.flag(this, 'object'),
+ ...normalizeArguments(propertiesOrHint, hint),
})
+ return assertMatchResult(result)
}),
)
}
@@ -96,33 +97,23 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => {
utils.addMethod(
chai.Assertion.prototype,
'toMatchFileSnapshot',
- function (this: Assertion, file: string, message?: string) {
+ function (this: Chai.Assertion, filepath: string, hint?: string) {
+ // set name manually since it's not wrapped by wrapAssertion
utils.flag(this, '_name', 'toMatchFileSnapshot')
- const isNot = utils.flag(this, 'negate')
- if (isNot) {
- throw new Error('toMatchFileSnapshot cannot be used with "not"')
- }
- const error = new Error('resolves')
- const expected = utils.flag(this, 'object')
- const test = getTest('toMatchFileSnapshot', this)
- const errorMessage = utils.flag(this, 'message')
-
- const promise = getSnapshotClient().assertRaw({
- received: expected,
- message,
- isInline: false,
- rawSnapshot: {
- file,
- },
- errorMessage,
- ...getTestNames(test),
+ // validate early synchronously just not to break some existing tests
+ validateAssertion(this)
+ const resultPromise = toMatchFileSnapshotImpl({
+ assertion: this,
+ received: utils.flag(this, 'object'),
+ filepath,
+ hint,
})
-
+ const assertPromise = resultPromise.then(result => assertMatchResult(result))
return recordAsyncExpect(
- test,
- promise,
+ getTest(this),
+ assertPromise,
createAssertionMessage(utils, this, true),
- error,
+ new Error('resolves'),
utils.flag(this, 'soft'),
)
},
@@ -133,61 +124,32 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => {
'toMatchInlineSnapshot',
wrapAssertion(utils, 'toMatchInlineSnapshot', function __INLINE_SNAPSHOT_OFFSET_3__(
this,
- properties?: object,
- inlineSnapshot?: string,
- message?: string,
+ propertiesOrInlineSnapshot?: object | string,
+ inlineSnapshotOrHint?: string,
+ hint?: string,
) {
- utils.flag(this, '_name', 'toMatchInlineSnapshot')
- const isNot = utils.flag(this, 'negate')
- if (isNot) {
- throw new Error('toMatchInlineSnapshot cannot be used with "not"')
- }
- const test = getTest('toMatchInlineSnapshot', this)
- const expected = utils.flag(this, 'object')
- const error = utils.flag(this, 'error')
- if (typeof properties === 'string') {
- message = inlineSnapshot
- inlineSnapshot = properties
- properties = undefined
- }
- if (inlineSnapshot) {
- inlineSnapshot = stripSnapshotIndentation(inlineSnapshot)
- }
- const errorMessage = utils.flag(this, 'message')
-
- getSnapshotClient().assert({
- received: expected,
- message,
+ const result = toMatchSnapshotImpl({
+ assertion: this,
+ received: utils.flag(this, 'object'),
isInline: true,
- properties,
- inlineSnapshot,
- error,
- errorMessage,
- ...getTestNames(test),
+ ...normalizeInlineArguments(propertiesOrInlineSnapshot, inlineSnapshotOrHint, hint),
})
+ return assertMatchResult(result)
}),
)
utils.addMethod(
chai.Assertion.prototype,
'toThrowErrorMatchingSnapshot',
- wrapAssertion(utils, 'toThrowErrorMatchingSnapshot', function (this, properties?: object, message?: string) {
- utils.flag(this, '_name', 'toThrowErrorMatchingSnapshot')
- 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 = getTest('toThrowErrorMatchingSnapshot', this)
+ wrapAssertion(utils, 'toThrowErrorMatchingSnapshot', function (this, propertiesOrHint?: object | string, hint?: string) {
+ validateAssertion(this)
+ const received = utils.flag(this, 'object')
const promise = utils.flag(this, 'promise') as string | undefined
- const errorMessage = utils.flag(this, 'message')
- getSnapshotClient().assert({
- received: getError(expected, promise),
- message,
- errorMessage,
- ...getTestNames(test),
+ const result = toMatchSnapshotImpl({
+ assertion: this,
+ received: getError(received, promise),
+ ...normalizeArguments(propertiesOrHint, hint),
})
+ return assertMatchResult(result)
}),
)
utils.addMethod(
@@ -195,35 +157,211 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => {
'toThrowErrorMatchingInlineSnapshot',
wrapAssertion(utils, 'toThrowErrorMatchingInlineSnapshot', function __INLINE_SNAPSHOT_OFFSET_3__(
this,
- inlineSnapshot: string,
- message: string,
+ inlineSnapshotOrHint?: string,
+ hint?: string,
) {
- const isNot = utils.flag(this, 'negate')
- if (isNot) {
- throw new Error(
- 'toThrowErrorMatchingInlineSnapshot cannot be used with "not"',
- )
- }
- const test = getTest('toThrowErrorMatchingInlineSnapshot', this)
- const expected = utils.flag(this, 'object')
- const error = utils.flag(this, 'error')
+ validateAssertion(this)
+ const received = utils.flag(this, 'object')
const promise = utils.flag(this, 'promise') as string | undefined
- const errorMessage = utils.flag(this, 'message')
-
- if (inlineSnapshot) {
- inlineSnapshot = stripSnapshotIndentation(inlineSnapshot)
- }
-
- getSnapshotClient().assert({
- received: getError(expected, promise),
- message,
- inlineSnapshot,
+ const result = toMatchSnapshotImpl({
+ assertion: this,
+ received: getError(received, promise),
isInline: true,
- error,
- errorMessage,
- ...getTestNames(test),
+ ...normalizeInlineArguments(undefined, inlineSnapshotOrHint, hint),
})
+ return assertMatchResult(result)
}),
)
utils.addMethod(chai.expect, 'addSnapshotSerializer', addSerializer)
}
+
+// toMatchSnapshot(propertiesOrHint?, hint?)
+function normalizeArguments(
+ propertiesOrHint?: object | string,
+ hint?: string,
+): { properties?: object; hint?: string } {
+ if (typeof propertiesOrHint === 'string') {
+ return { hint: propertiesOrHint }
+ }
+ return { properties: propertiesOrHint, hint }
+}
+
+// toMatchInlineSnapshot(propertiesOrInlineSnapshot?, inlineSnapshotOrHint?, hint?)
+function normalizeInlineArguments(
+ propertiesOrInlineSnapshot?: object | string,
+ inlineSnapshotOrHint?: string,
+ hint?: string,
+): { properties?: object; inlineSnapshot?: string; hint?: string } {
+ let inlineSnapshot: string | undefined
+ if (typeof propertiesOrInlineSnapshot === 'string') {
+ inlineSnapshot = stripSnapshotIndentation(propertiesOrInlineSnapshot)
+ return { inlineSnapshot, hint: inlineSnapshotOrHint }
+ }
+ if (inlineSnapshotOrHint) {
+ inlineSnapshot = stripSnapshotIndentation(inlineSnapshotOrHint)
+ }
+ return { properties: propertiesOrInlineSnapshot, inlineSnapshot, hint }
+}
+
+function toMatchSnapshotImpl(options: {
+ assertion: Chai.Assertion
+ received: unknown
+ properties?: object
+ hint?: string
+ isInline?: boolean
+ inlineSnapshot?: string
+}): SyncExpectationResult {
+ const { assertion } = options
+ validateAssertion(assertion)
+ const assertionName = getAssertionName(assertion)
+ const test = getTest(assertion)
+ return getSnapshotClient().match({
+ received: options.received,
+ properties: options.properties,
+ message: options.hint,
+ isInline: options.isInline,
+ inlineSnapshot: options.inlineSnapshot,
+ errorMessage: chai.util.flag(assertion, 'message'),
+ // pass `assertionName` for inline snapshot stack probing
+ assertionName,
+ // set by async assertion (e.g. resolves/rejects) for inline snapshot stack probing
+ error: chai.util.flag(assertion, 'error'),
+ ...getTestNames(test),
+ })
+}
+
+async function toMatchFileSnapshotImpl(options: {
+ assertion: Chai.Assertion
+ received: unknown
+ filepath: string
+ hint?: string
+}): Promise {
+ const { assertion } = options
+ validateAssertion(assertion)
+ const test = getTest(assertion)
+ const testNames = getTestNames(test)
+ const snapshotState = getSnapshotClient().getSnapshotState(testNames.filepath)
+ const rawSnapshotFile = await snapshotState.environment.resolveRawPath(testNames.filepath, options.filepath)
+ const rawSnapshotContent = await snapshotState.environment.readSnapshotFile(rawSnapshotFile)
+ return getSnapshotClient().match({
+ received: options.received,
+ message: options.hint,
+ errorMessage: chai.util.flag(assertion, 'message'),
+ rawSnapshot: {
+ file: rawSnapshotFile,
+ content: rawSnapshotContent ?? undefined,
+ },
+ ...testNames,
+ })
+}
+
+function assertMatchResult(result: SyncExpectationResult): void {
+ if (!result.pass) {
+ throw Object.assign(new Error(result.message()), {
+ actual: result.actual,
+ expected: result.expected,
+ diffOptions: {
+ expand: getWorkerState().config.snapshotOptions.expand,
+ },
+ })
+ }
+}
+
+/**
+ * Composable for building custom snapshot matchers via `expect.extend`.
+ * Call with `this` bound to the matcher state. Returns `{ pass, message }`
+ * compatible with the custom matcher return contract.
+ *
+ * @example
+ * ```ts
+ * import { toMatchSnapshot } from 'vitest/runtime'
+ *
+ * expect.extend({
+ * toMatchTrimmedSnapshot(received: string) {
+ * return toMatchSnapshot.call(this, received.slice(0, 10))
+ * },
+ * })
+ * ```
+ *
+ * @experimental
+ * @see https://vitest.dev/guide/snapshot.html#custom-snapshot-matchers
+ */
+export function toMatchSnapshot(
+ this: MatcherState,
+ received: unknown,
+ propertiesOrHint?: object | string,
+ hint?: string,
+): SyncExpectationResult {
+ return toMatchSnapshotImpl({
+ assertion: this.__vitest_assertion__,
+ received,
+ ...normalizeArguments(propertiesOrHint, hint),
+ })
+}
+
+/**
+ * Composable for building custom inline snapshot matchers via `expect.extend`.
+ * Call with `this` bound to the matcher state. Returns `{ pass, message }`
+ * compatible with the custom matcher return contract.
+ *
+ * @example
+ * ```ts
+ * import { toMatchInlineSnapshot } from 'vitest/runtime'
+ *
+ * expect.extend({
+ * toMatchTrimmedInlineSnapshot(received: string, inlineSnapshot?: string) {
+ * return toMatchInlineSnapshot.call(this, received.slice(0, 10), inlineSnapshot)
+ * },
+ * })
+ * ```
+ *
+ * @experimental
+ * @see https://vitest.dev/guide/snapshot.html#custom-snapshot-matchers
+ */
+export function toMatchInlineSnapshot(
+ this: MatcherState,
+ received: unknown,
+ propertiesOrInlineSnapshot?: object | string,
+ inlineSnapshotOrHint?: string,
+ hint?: string,
+): SyncExpectationResult {
+ return toMatchSnapshotImpl({
+ assertion: this.__vitest_assertion__,
+ received,
+ isInline: true,
+ ...normalizeInlineArguments(propertiesOrInlineSnapshot, inlineSnapshotOrHint, hint),
+ })
+}
+
+/**
+ * Composable for building custom file snapshot matchers via `expect.extend`.
+ * Call with `this` bound to the matcher state. Returns a `Promise<{ pass, message }>`
+ * compatible with the custom matcher return contract.
+ *
+ * @example
+ * ```ts
+ * import { toMatchFileSnapshot } from 'vitest/runtime'
+ *
+ * expect.extend({
+ * async toMatchTrimmedFileSnapshot(received: string, file: string) {
+ * return toMatchFileSnapshot.call(this, received.slice(0, 10), file)
+ * },
+ * })
+ * ```
+ *
+ * @experimental
+ * @see https://vitest.dev/guide/snapshot.html#custom-snapshot-matchers
+ */
+export function toMatchFileSnapshot(
+ this: MatcherState,
+ received: unknown,
+ filepath: string,
+ hint?: string,
+): Promise {
+ return toMatchFileSnapshotImpl({
+ assertion: this.__vitest_assertion__,
+ received,
+ filepath,
+ hint,
+ })
+}
diff --git a/packages/vitest/src/public/runtime.ts b/packages/vitest/src/public/runtime.ts
index f8d048111bf7..4fbf36da6fc0 100644
--- a/packages/vitest/src/public/runtime.ts
+++ b/packages/vitest/src/public/runtime.ts
@@ -11,6 +11,7 @@ import { getWorkerState } from '../runtime/utils'
export { environments as builtinEnvironments } from '../integrations/env/index'
export { populateGlobal } from '../integrations/env/utils'
+export { toMatchFileSnapshot, toMatchInlineSnapshot, toMatchSnapshot } from '../integrations/snapshot/chai'
export { VitestNodeSnapshotEnvironment as VitestSnapshotEnvironment } from '../integrations/snapshot/environments/node'
export type {
Environment,
diff --git a/test/core/test/exports.test.ts b/test/core/test/exports.test.ts
index 1829c30a757c..93aaf0555edc 100644
--- a/test/core/test/exports.test.ts
+++ b/test/core/test/exports.test.ts
@@ -165,6 +165,9 @@ it('exports snapshot', async ({ skip, task }) => {
"__INTERNAL": "object",
"builtinEnvironments": "object",
"populateGlobal": "function",
+ "toMatchFileSnapshot": "function",
+ "toMatchInlineSnapshot": "function",
+ "toMatchSnapshot": "function",
},
"./snapshot": {
"VitestSnapshotEnvironment": "function",
diff --git a/test/snapshots/test/custom-matcher.test.ts b/test/snapshots/test/custom-matcher.test.ts
new file mode 100644
index 000000000000..a0226ac2f0ab
--- /dev/null
+++ b/test/snapshots/test/custom-matcher.test.ts
@@ -0,0 +1,338 @@
+import fs, { readFileSync } from 'node:fs'
+import { join } from 'node:path'
+import { expect, test } from 'vitest'
+import { editFile, runVitest } from '../../test-utils'
+
+const INLINE_BLOCK_RE = /\/\/ -- TEST INLINE START --\n([\s\S]*?)\/\/ -- TEST INLINE END --/g
+
+function extractInlineBlocks(content: string): string {
+ return [...content.matchAll(INLINE_BLOCK_RE)]
+ .map(m => m[1].trim())
+ .join('\n\n')
+}
+
+test('custom snapshot matcher', async () => {
+ const root = join(import.meta.dirname, 'fixtures/custom-matcher')
+ const testFile = join(root, 'basic.test.ts')
+ const snapshotFile = join(root, '__snapshots__/basic.test.ts.snap')
+ const rawSnapshotFile = join(root, '__snapshots__/raw.txt')
+
+ // remove snapshots
+ fs.rmSync(join(root, '__snapshots__'), { recursive: true, force: true })
+ editFile(testFile, s => s.replace(/toMatchCustomInlineSnapshot\(`[^`]*`\)/g, 'toMatchCustomInlineSnapshot()'))
+
+ // create snapshots from scratch
+ 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[\`file 1\`] = \`
+ Object {
+ "length": 6,
+ "reversed": "ahahah",
+ }
+ \`;
+
+ exports[\`properties 1 1\`] = \`
+ Object {
+ "length": 6,
+ "reversed": "opopop",
+ }
+ \`;
+
+ exports[\`properties 2 1\`] = \`
+ Object {
+ "length": toSatisfy<[Function lessThan10]>,
+ "reversed": "epepep",
+ }
+ \`;
+ "
+ `)
+ expect(readFileSync(rawSnapshotFile, 'utf-8')).toMatchInlineSnapshot(`
+ "Object {
+ "length": 6,
+ "reversed": "ihihih",
+ }"
+ `)
+ expect(extractInlineBlocks(readFileSync(testFile, 'utf-8'))).toMatchInlineSnapshot(`
+ "test('inline', () => {
+ expect(\`hehehe\`).toMatchCustomInlineSnapshot(\`
+ Object {
+ "length": 6,
+ "reversed": "eheheh",
+ }
+ \`)
+ })"
+ `)
+ expect(result.errorTree()).toMatchInlineSnapshot(`
+ Object {
+ "basic.test.ts": Object {
+ "file": "passed",
+ "inline": "passed",
+ "properties 1": "passed",
+ "properties 2": "passed",
+ "raw": "passed",
+ },
+ }
+ `)
+
+ // edit tests to introduce snapshot errors
+ editFile(testFile, s => s
+ .replace('`hahaha`', '`hahaha-edit`')
+ .replace('`popopo`', '`popopo-edit`')
+ .replace('`pepepe`', '`pepepe-edit`')
+ .replace('`hihihi`', '`hihihi-edit`')
+ .replace('`hehehe`', '`hehehe-edit`'))
+
+ result = await runVitest({ root, update: 'none' })
+ expect(result.stderr).toMatchInlineSnapshot(`
+ "
+ ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 5 ⎯⎯⎯⎯⎯⎯⎯
+
+ FAIL basic.test.ts > file
+ Error: [custom error] Snapshot \`file 1\` mismatched
+
+ - Expected
+ + Received
+
+ Object {
+ - "length": 6,
+ + "length": 11,
+ - "reversed": "ahahah",
+ + "reversed": "tide-ahahah",
+ }
+
+ ❯ basic.test.ts:46:25
+ 44|
+ 45| test('file', () => {
+ 46| expect(\`hahaha-edit\`).toMatchCustomSnapshot()
+ | ^
+ 47| })
+ 48|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/5]⎯
+
+ FAIL basic.test.ts > properties 1
+ Error: [custom error] Snapshot properties mismatched
+
+ - Expected
+ + Received
+
+ {
+ - "length": 6,
+ + "length": 11,
+ + "reversed": "tide-opopop",
+ }
+
+ ❯ basic.test.ts:50:25
+ 48|
+ 49| test('properties 1', () => {
+ 50| expect(\`popopo-edit\`).toMatchCustomSnapshot({ length: 6 })
+ | ^
+ 51| })
+ 52|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/5]⎯
+
+ FAIL basic.test.ts > properties 2
+ Error: [custom error] Snapshot properties mismatched
+
+ - Expected
+ + Received
+
+ {
+ - "length": toSatisfy<[Function lessThan10]>,
+ + "length": 11,
+ + "reversed": "tide-epepep",
+ }
+
+ ❯ basic.test.ts:54:25
+ 52|
+ 53| test('properties 2', () => {
+ 54| expect(\`pepepe-edit\`).toMatchCustomSnapshot({ length: expect.toSatis…
+ | ^
+ 55| })
+ 56|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/5]⎯
+
+ FAIL basic.test.ts > raw
+ Error: [custom error] Snapshot \`raw 1\` mismatched
+
+ - Expected
+ + Received
+
+ Object {
+ - "length": 6,
+ + "length": 11,
+ - "reversed": "ihihih",
+ + "reversed": "tide-ihihih",
+ }
+
+ ❯ basic.test.ts:58:3
+ 56|
+ 57| test('raw', async () => {
+ 58| await expect(\`hihihi-edit\`).toMatchCustomFileSnapshot('./__snapshots…
+ | ^
+ 59| })
+ 60|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/5]⎯
+
+ FAIL basic.test.ts > inline
+ Error: [custom error] Snapshot \`inline 1\` mismatched
+
+ - Expected
+ + Received
+
+ Object {
+ - "length": 6,
+ + "length": 11,
+ - "reversed": "eheheh",
+ + "reversed": "tide-eheheh",
+ }
+
+ ❯ basic.test.ts:63:25
+ 61| // -- TEST INLINE START --
+ 62| test('inline', () => {
+ 63| expect(\`hehehe-edit\`).toMatchCustomInlineSnapshot(\`
+ | ^
+ 64| Object {
+ 65| "length": 6,
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/5]⎯
+
+ "
+ `)
+ expect(result.errorTree()).toMatchInlineSnapshot(`
+ Object {
+ "basic.test.ts": Object {
+ "file": Array [
+ "[custom error] Snapshot \`file 1\` mismatched",
+ ],
+ "inline": Array [
+ "[custom error] Snapshot \`inline 1\` mismatched",
+ ],
+ "properties 1": Array [
+ "[custom error] Snapshot properties mismatched",
+ ],
+ "properties 2": Array [
+ "[custom error] Snapshot properties mismatched",
+ ],
+ "raw": Array [
+ "[custom error] Snapshot \`raw 1\` mismatched",
+ ],
+ },
+ }
+ `)
+
+ // run with update
+ result = await runVitest({ root, update: 'all' })
+ expect(result.stderr).toMatchInlineSnapshot(`
+ "
+ ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
+
+ FAIL basic.test.ts > properties 1
+ Error: [custom error] Snapshot properties mismatched
+
+ - Expected
+ + Received
+
+ {
+ - "length": 6,
+ + "length": 11,
+ + "reversed": "tide-opopop",
+ }
+
+ ❯ basic.test.ts:50:25
+ 48|
+ 49| test('properties 1', () => {
+ 50| expect(\`popopo-edit\`).toMatchCustomSnapshot({ length: 6 })
+ | ^
+ 51| })
+ 52|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
+
+ FAIL basic.test.ts > properties 2
+ Error: [custom error] Snapshot properties mismatched
+
+ - Expected
+ + Received
+
+ {
+ - "length": toSatisfy<[Function lessThan10]>,
+ + "length": 11,
+ + "reversed": "tide-epepep",
+ }
+
+ ❯ basic.test.ts:54:25
+ 52|
+ 53| test('properties 2', () => {
+ 54| expect(\`pepepe-edit\`).toMatchCustomSnapshot({ length: expect.toSatis…
+ | ^
+ 55| })
+ 56|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯
+
+ "
+ `)
+ expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(`
+ "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+ exports[\`file 1\`] = \`
+ Object {
+ "length": 11,
+ "reversed": "tide-ahahah",
+ }
+ \`;
+
+ exports[\`properties 1 1\`] = \`
+ Object {
+ "length": 6,
+ "reversed": "opopop",
+ }
+ \`;
+
+ exports[\`properties 2 1\`] = \`
+ Object {
+ "length": toSatisfy<[Function lessThan10]>,
+ "reversed": "epepep",
+ }
+ \`;
+ "
+ `)
+ expect(readFileSync(rawSnapshotFile, 'utf-8')).toMatchInlineSnapshot(`
+ "Object {
+ "length": 11,
+ "reversed": "tide-ihihih",
+ }"
+ `)
+ expect(extractInlineBlocks(readFileSync(testFile, 'utf-8'))).toMatchInlineSnapshot(`
+ "test('inline', () => {
+ expect(\`hehehe-edit\`).toMatchCustomInlineSnapshot(\`
+ Object {
+ "length": 11,
+ "reversed": "tide-eheheh",
+ }
+ \`)
+ })"
+ `)
+ expect(result.errorTree()).toMatchInlineSnapshot(`
+ Object {
+ "basic.test.ts": Object {
+ "file": "passed",
+ "inline": "passed",
+ "properties 1": Array [
+ "[custom error] Snapshot properties mismatched",
+ ],
+ "properties 2": Array [
+ "[custom error] Snapshot properties mismatched",
+ ],
+ "raw": "passed",
+ },
+ }
+ `)
+})
diff --git a/test/snapshots/test/fixtures/custom-matcher/.gitignore b/test/snapshots/test/fixtures/custom-matcher/.gitignore
new file mode 100644
index 000000000000..b05c2dfa7007
--- /dev/null
+++ b/test/snapshots/test/fixtures/custom-matcher/.gitignore
@@ -0,0 +1 @@
+__snapshots__
diff --git a/test/snapshots/test/fixtures/custom-matcher/basic.test.ts b/test/snapshots/test/fixtures/custom-matcher/basic.test.ts
new file mode 100644
index 000000000000..1da84b6d0346
--- /dev/null
+++ b/test/snapshots/test/fixtures/custom-matcher/basic.test.ts
@@ -0,0 +1,70 @@
+import { expect, test } from 'vitest'
+import { toMatchFileSnapshot, toMatchInlineSnapshot, toMatchSnapshot } from "vitest/runtime"
+
+// custom snapshot matcher to wraper input code string
+interface CustomMatchers {
+ toMatchCustomSnapshot: (properties?: object) => R
+ toMatchCustomInlineSnapshot: (snapshot?: string) => R
+ toMatchCustomFileSnapshot: (filepath: string) => Promise
+}
+
+declare module 'vitest' {
+ interface Assertion extends CustomMatchers {}
+ interface AsymmetricMatchersContaining extends CustomMatchers {}
+}
+
+function formatCustom(input: string) {
+ return {
+ reversed: input.split('').reverse().join(''),
+ length: input.length,
+ }
+}
+
+expect.extend({
+ toMatchCustomSnapshot(actual: string, properties?: object) {
+ const actualCustom = formatCustom(actual)
+ const result = toMatchSnapshot.call(this, actualCustom, properties)
+ // result can be further enhanced
+ return { ...result, message: () => `[custom error] ${result.message()}` }
+ },
+ toMatchCustomInlineSnapshot(
+ actual: string,
+ inlineSnapshot?: string,
+ ) {
+ const actualCustom = formatCustom(actual)
+ const result = toMatchInlineSnapshot.call(this, actualCustom, inlineSnapshot)
+ return { ...result, message: () => `[custom error] ${result.message()}` }
+ },
+ async toMatchCustomFileSnapshot(actual: string, filepath: string) {
+ const actualCustom = formatCustom(actual)
+ const result = await toMatchFileSnapshot.call(this, actualCustom, filepath)
+ return { ...result, message: () => `[custom error] ${result.message()}` }
+ },
+})
+
+test('file', () => {
+ expect(`hahaha`).toMatchCustomSnapshot()
+})
+
+test('properties 1', () => {
+ expect(`popopo`).toMatchCustomSnapshot({ length: 6 })
+})
+
+test('properties 2', () => {
+ expect(`pepepe`).toMatchCustomSnapshot({ length: expect.toSatisfy(function lessThan10(n) { return n < 10 }) })
+})
+
+test('raw', async () => {
+ await expect(`hihihi`).toMatchCustomFileSnapshot('./__snapshots__/raw.txt')
+})
+
+// -- TEST INLINE START --
+test('inline', () => {
+ expect(`hehehe`).toMatchCustomInlineSnapshot(`
+ Object {
+ "length": 6,
+ "reversed": "eheheh",
+ }
+ `)
+})
+// -- TEST INLINE END --