Skip to content

Commit f691ad7

Browse files
authoredDec 9, 2024··
feat(expect): add toSatisfy asymmetric matcher (#7022)
1 parent b3e43d0 commit f691ad7

File tree

7 files changed

+158
-17
lines changed

7 files changed

+158
-17
lines changed
 
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { MatchersObject } from './types'
2+
3+
// selectively ported from https://github.com/jest-community/jest-extended
4+
export const customMatchers: MatchersObject = {
5+
toSatisfy(actual: unknown, expected: (actual: unknown) => boolean, message?: string) {
6+
const { printReceived, printExpected, matcherHint } = this.utils
7+
const pass = expected(actual)
8+
return {
9+
pass,
10+
message: () =>
11+
pass
12+
? `\
13+
${matcherHint('.not.toSatisfy', 'received', '')}
14+
15+
Expected value to not satisfy:
16+
${message || printExpected(expected)}
17+
Received:
18+
${printReceived(actual)}`
19+
: `\
20+
${matcherHint('.toSatisfy', 'received', '')}
21+
22+
Expected value to satisfy:
23+
${message || printExpected(expected)}
24+
25+
Received:
26+
${printReceived(actual)}`,
27+
}
28+
},
29+
}

‎packages/expect/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './constants'
2+
export { customMatchers } from './custom-matchers'
23
export * from './jest-asymmetric-matchers'
34
export { JestChaiExpect } from './jest-expect'
45
export { JestExtend } from './jest-extend'

‎packages/expect/src/jest-expect.ts

-3
Original file line numberDiff line numberDiff line change
@@ -1029,9 +1029,6 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
10291029
)
10301030
})
10311031
})
1032-
def('toSatisfy', function (matcher: Function, message?: string) {
1033-
return this.be.satisfy(matcher, message)
1034-
})
10351032

10361033
// @ts-expect-error @internal
10371034
def('withContext', function (this: any, context: Record<string, any>) {

‎packages/expect/src/types.ts

+16-13
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,21 @@ export interface ExpectStatic
106106
not: AsymmetricMatchersContaining
107107
}
108108

109-
export interface AsymmetricMatchersContaining {
109+
interface CustomMatcher {
110+
/**
111+
* Checks that a value satisfies a custom matcher function.
112+
*
113+
* @param matcher - A function returning a boolean based on the custom condition
114+
* @param message - Optional custom error message on failure
115+
*
116+
* @example
117+
* expect(age).toSatisfy(val => val >= 18, 'Age must be at least 18');
118+
* expect(age).toEqual(expect.toSatisfy(val => val >= 18, 'Age must be at least 18'));
119+
*/
120+
toSatisfy: (matcher: (value: any) => boolean, message?: string) => any
121+
}
122+
123+
export interface AsymmetricMatchersContaining extends CustomMatcher {
110124
/**
111125
* Matches if the received string contains the expected substring.
112126
*
@@ -153,7 +167,7 @@ export interface AsymmetricMatchersContaining {
153167
closeTo: (expected: number, precision?: number) => any
154168
}
155169

156-
export interface JestAssertion<T = any> extends jest.Matchers<void, T> {
170+
export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMatcher {
157171
/**
158172
* Used when you want to check that two objects have the same value.
159173
* This matcher recursively checks the equality of all fields, rather than checking for object identity.
@@ -645,17 +659,6 @@ export interface Assertion<T = any>
645659
*/
646660
toHaveBeenCalledExactlyOnceWith: <E extends any[]>(...args: E) => void
647661

648-
/**
649-
* Checks that a value satisfies a custom matcher function.
650-
*
651-
* @param matcher - A function returning a boolean based on the custom condition
652-
* @param message - Optional custom error message on failure
653-
*
654-
* @example
655-
* expect(age).toSatisfy(val => val >= 18, 'Age must be at least 18');
656-
*/
657-
toSatisfy: <E>(matcher: (value: E) => boolean, message?: string) => void
658-
659662
/**
660663
* This assertion checks if a `Mock` was called before another `Mock`.
661664
* @param mock - A mock function created by `vi.spyOn` or `vi.fn`

‎packages/vitest/src/integrations/chai/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { TaskPopulated, Test } from '@vitest/runner'
55
import {
66
addCustomEqualityTesters,
77
ASYMMETRIC_MATCHERS_OBJECT,
8+
customMatchers,
89
getState,
910
GLOBAL_EXPECT,
1011
setState,
@@ -109,6 +110,8 @@ export function createExpect(test?: TaskPopulated) {
109110
chai.util.addMethod(expect, 'assertions', assertions)
110111
chai.util.addMethod(expect, 'hasAssertions', hasAssertions)
111112

113+
expect.extend(customMatchers)
114+
112115
return expect
113116
}
114117

‎test/core/test/__snapshots__/jest-expect.test.ts.snap

+68
Original file line numberDiff line numberDiff line change
@@ -454,3 +454,71 @@ exports[`toMatch/toContain diff 3`] = `
454454
"message": "expected 'hellohellohellohellohellohellohellohe…' to match /world/",
455455
}
456456
`;
457+
458+
exports[`toSatisfy() > error message 1`] = `
459+
{
460+
"actual": "undefined",
461+
"diff": undefined,
462+
"expected": "undefined",
463+
"message": "expect(received).toSatisfy()
464+
465+
Expected value to satisfy:
466+
[Function isOdd]
467+
468+
Received:
469+
2",
470+
}
471+
`;
472+
473+
exports[`toSatisfy() > error message 2`] = `
474+
{
475+
"actual": "undefined",
476+
"diff": undefined,
477+
"expected": "undefined",
478+
"message": "expect(received).toSatisfy()
479+
480+
Expected value to satisfy:
481+
ODD
482+
483+
Received:
484+
2",
485+
}
486+
`;
487+
488+
exports[`toSatisfy() > error message 3`] = `
489+
{
490+
"actual": "Object {
491+
"value": 2,
492+
}",
493+
"diff": "- Expected
494+
+ Received
495+
496+
Object {
497+
- "value": toSatisfy<(value) => value % 2 !== 0>,
498+
+ "value": 2,
499+
}",
500+
"expected": "Object {
501+
"value": toSatisfy<(value) => value % 2 !== 0>,
502+
}",
503+
"message": "expected { value: 2 } to deeply equal { value: toSatisfy{…} }",
504+
}
505+
`;
506+
507+
exports[`toSatisfy() > error message 4`] = `
508+
{
509+
"actual": "Object {
510+
"value": 2,
511+
}",
512+
"diff": "- Expected
513+
+ Received
514+
515+
Object {
516+
- "value": toSatisfy<(value) => value % 2 !== 0, ODD>,
517+
+ "value": 2,
518+
}",
519+
"expected": "Object {
520+
"value": toSatisfy<(value) => value % 2 !== 0, ODD>,
521+
}",
522+
"message": "expected { value: 2 } to deeply equal { value: toSatisfy{…} }",
523+
}
524+
`;

‎test/core/test/jest-expect.test.ts

+41-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { AssertionError } from 'node:assert'
33
import { stripVTControlCharacters } from 'node:util'
44
import { generateToBeMessage } from '@vitest/expect'
55
import { processError } from '@vitest/utils/error'
6-
import { beforeAll, describe, expect, it, vi } from 'vitest'
6+
import { assert, beforeAll, describe, expect, it, vi } from 'vitest'
77

88
class TestError extends Error {}
99

@@ -606,6 +606,46 @@ describe('toSatisfy()', () => {
606606
expect(1).toSatisfy(isOddMock)
607607
expect(isOddMock).toBeCalled()
608608
})
609+
610+
it('asymmetric matcher', () => {
611+
expect({ value: 1 }).toEqual({ value: expect.toSatisfy(isOdd) })
612+
expect(() => {
613+
expect({ value: 2 }).toEqual({ value: expect.toSatisfy(isOdd, 'odd') })
614+
}).toThrowErrorMatchingInlineSnapshot(
615+
`[AssertionError: expected { value: 2 } to deeply equal { value: toSatisfy{…} }]`,
616+
)
617+
618+
expect(() => {
619+
throw new Error('1')
620+
}).toThrow(
621+
expect.toSatisfy((e) => {
622+
assert(e instanceof Error)
623+
expect(e).toMatchObject({ message: expect.toSatisfy(isOdd) })
624+
return true
625+
}),
626+
)
627+
628+
expect(() => {
629+
expect(() => {
630+
throw new Error('2')
631+
}).toThrow(
632+
expect.toSatisfy((e) => {
633+
assert(e instanceof Error)
634+
expect(e).toMatchObject({ message: expect.toSatisfy(isOdd) })
635+
return true
636+
}),
637+
)
638+
}).toThrowErrorMatchingInlineSnapshot(
639+
`[AssertionError: expected Error: 2 to match object { message: toSatisfy{…} }]`,
640+
)
641+
})
642+
643+
it('error message', () => {
644+
snapshotError(() => expect(2).toSatisfy(isOdd))
645+
snapshotError(() => expect(2).toSatisfy(isOdd, 'ODD'))
646+
snapshotError(() => expect({ value: 2 }).toEqual({ value: expect.toSatisfy(isOdd) }))
647+
snapshotError(() => expect({ value: 2 }).toEqual({ value: expect.toSatisfy(isOdd, 'ODD') }))
648+
})
609649
})
610650

611651
describe('toHaveBeenCalled', () => {

0 commit comments

Comments
 (0)
Please sign in to comment.