Skip to content

Commit 8ec448d

Browse files
fix: switch ExpectStatic any types to AsymmetricMatcher<unknown>, with DeeplyAllowMatchers<T> (#7016)
1 parent e6fbd8d commit 8ec448d

File tree

5 files changed

+138
-7
lines changed

5 files changed

+138
-7
lines changed

packages/expect/src/types.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import type { MockInstance } from '@vitest/spy'
1010
import type { Constructable } from '@vitest/utils'
1111
import type { Formatter } from 'tinyrainbow'
12+
import type { AsymmetricMatcher } from './jest-asymmetric-matchers'
1213
import type { diff, getMatcherUtils, stringify } from './jest-matcher-utils'
1314

1415
export type ChaiPlugin = Chai.ChaiPlugin
@@ -99,8 +100,8 @@ export interface ExpectStatic
99100
AsymmetricMatchersContaining {
100101
<T>(actual: T, message?: string): Assertion<T>
101102
extend: (expects: MatchersObject) => void
102-
anything: () => any
103-
any: (constructor: unknown) => any
103+
anything: () => AsymmetricMatcher<unknown>
104+
any: (constructor: unknown) => AsymmetricMatcher<unknown>
104105
getState: () => MatcherState
105106
setState: (state: Partial<MatcherState>) => void
106107
not: AsymmetricMatchersContaining
@@ -146,15 +147,15 @@ export interface AsymmetricMatchersContaining extends CustomMatcher {
146147
* @example
147148
* expect({ a: '1', b: 2 }).toEqual(expect.objectContaining({ a: '1' }))
148149
*/
149-
objectContaining: <T = any>(expected: T) => any
150+
objectContaining: <T = any>(expected: DeeplyAllowMatchers<T>) => any
150151

151152
/**
152153
* Matches if the received array contains all elements in the expected array.
153154
*
154155
* @example
155156
* expect(['a', 'b', 'c']).toEqual(expect.arrayContaining(['b', 'a']));
156157
*/
157-
arrayContaining: <T = unknown>(expected: Array<T>) => any
158+
arrayContaining: <T = unknown>(expected: Array<DeeplyAllowMatchers<T>>) => any
158159

159160
/**
160161
* Matches if the received string or regex matches the expected pattern.
@@ -177,6 +178,14 @@ export interface AsymmetricMatchersContaining extends CustomMatcher {
177178
closeTo: (expected: number, precision?: number) => any
178179
}
179180

181+
export type DeeplyAllowMatchers<T> = T extends Array<infer Element>
182+
? (DeeplyAllowMatchers<Element> | Element)[]
183+
: T extends object
184+
? {
185+
[K in keyof T]: DeeplyAllowMatchers<T[K]> | AsymmetricMatcher<unknown>
186+
}
187+
: T
188+
180189
export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMatcher {
181190
/**
182191
* Used when you want to check that two objects have the same value.
@@ -185,15 +194,15 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
185194
* @example
186195
* expect(user).toEqual({ name: 'Alice', age: 30 });
187196
*/
188-
toEqual: <E>(expected: E) => void
197+
toEqual: <E>(expected: DeeplyAllowMatchers<E>) => void
189198

190199
/**
191200
* Use to test that objects have the same types as well as structure.
192201
*
193202
* @example
194203
* expect(user).toStrictEqual({ name: 'Alice', age: 30 });
195204
*/
196-
toStrictEqual: <E>(expected: E) => void
205+
toStrictEqual: <E>(expected: DeeplyAllowMatchers<E>) => void
197206

198207
/**
199208
* Checks that a value is what you expect. It calls `Object.is` to compare values.
@@ -223,7 +232,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
223232
* address: { city: 'Wonderland' }
224233
* });
225234
*/
226-
toMatchObject: <E extends object | any[]>(expected: E) => void
235+
toMatchObject: <E extends object | any[]>(expected: DeeplyAllowMatchers<E>) => void
227236

228237
/**
229238
* Used when you want to check that an item is in a list.

test/core/test/expect.test-d.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/* eslint-disable no-lone-blocks */
2+
3+
import { expect, test } from 'vitest'
4+
5+
test('expect.* allows asymmetrict mattchers with different types', () => {
6+
// types.ts examples: stringContaining
7+
expect('I have an apple').toEqual(expect.stringContaining('apple'))
8+
expect('I have an apple').toEqual<string>(expect.stringContaining('apple'))
9+
10+
expect({ a: 'test string' }).toEqual({ a: expect.stringContaining('test') })
11+
expect({ a: 'test string' }).toEqual<{ a: string }>({ a: expect.stringContaining('test') })
12+
13+
// types.ts examples: objectContaining
14+
expect({ a: '1', b: 2 }).toEqual(expect.objectContaining({ a: '1' }))
15+
expect({ a: '1', b: 2 }).toEqual<{ a: string; b: string }>(expect.objectContaining({ a: '1' }))
16+
17+
// types.ts examples: arrayContaining
18+
expect(['a', 'b', 'c']).toEqual(expect.arrayContaining(['b', 'a']))
19+
expect(['a', 'b', 'c']).toEqual<string[]>(expect.arrayContaining(['b', 'a']))
20+
21+
// types.ts examples: stringMatching
22+
expect('hello world').toEqual(expect.stringMatching(/^hello/))
23+
expect('hello world').toEqual<string>(expect.stringMatching(/^hello/))
24+
25+
expect('hello world').toEqual(expect.stringMatching('hello'))
26+
expect('hello world').toEqual<string>(expect.stringMatching('hello'))
27+
28+
// types.ts examples: closeTo
29+
expect(10.45).toEqual(expect.closeTo(10.5, 1))
30+
expect(10.45).toEqual<number>(expect.closeTo(10.5, 1))
31+
32+
expect(5.11).toEqual(expect.closeTo(5.12))
33+
expect(5.11).toEqual<number>(expect.closeTo(5.12))
34+
35+
// expect.any(String)
36+
// https://github.com/vitest-dev/vitest/pull/7016#issuecomment-2517674066
37+
{
38+
const obj = {
39+
id: '',
40+
name: '',
41+
}
42+
43+
expect(obj).toEqual({
44+
id: expect.any(String),
45+
name: 'Amelia',
46+
})
47+
48+
expect(obj).toEqual<{
49+
id: string
50+
name: string
51+
}>({
52+
id: expect.any(String),
53+
name: 'Amelia',
54+
})
55+
}
56+
57+
// expect.any(Date)
58+
// https://github.com/vitest-dev/vitest/issues/4543#issuecomment-1817960296
59+
// https://github.com/vitest-dev/vitest/issues/4543#issuecomment-1817967628
60+
// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/62831#issue-1418959169
61+
{
62+
const actual = {} as {
63+
foo: string
64+
bar: string
65+
createdAt: Date
66+
}
67+
68+
expect(actual).toEqual({
69+
foo: 'foo',
70+
bar: 'bar',
71+
createdAt: expect.any(Date),
72+
})
73+
74+
expect(actual).toEqual<{
75+
foo: string
76+
bar: string
77+
createdAt: Date
78+
}>({
79+
foo: 'foo',
80+
bar: 'bar',
81+
createdAt: expect.any(Date),
82+
})
83+
84+
expect(actual).toEqual<{
85+
foo: string
86+
bar: string
87+
createdAt: Date
88+
}[]>([
89+
{
90+
foo: 'foo',
91+
bar: 'bar',
92+
createdAt: expect.any(Date),
93+
},
94+
])
95+
}
96+
97+
// expect.arrayContaining
98+
// https://github.com/jestjs/jest/issues/13812#issue-1555843276
99+
{
100+
expect([1, 2, 3]).toEqual(expect.arrayContaining(['a']))
101+
expect([1, 2, 3]).toEqual<number[]>(expect.arrayContaining(['a']))
102+
103+
expect([1, 2, 3]).toEqual(expect.arrayContaining([expect.any(Number)]))
104+
expect([1, 2, 3]).toEqual<number[]>(expect.arrayContaining([expect.any(Number)]))
105+
106+
expect([1, 2, 3]).toEqual(expect.arrayContaining([expect.anything()]))
107+
expect([1, 2, 3]).toEqual<number[]>(expect.arrayContaining([expect.anything()]))
108+
}
109+
110+
// expect.any(Array)
111+
// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/62831/files#diff-ff7b882e4a29e7fe0e348a6bdf8b11774d606eaa221009b166b01389576d921fR1237
112+
expect({ list: [1, 2, 3] }).toMatchObject({ list: expect.any(Array) })
113+
expect({ list: [1, 2, 3] }).toMatchObject<{ list: number[] }>({ list: expect.any(Array) })
114+
})
File renamed without changes.

test/core/tsconfig.typecheck.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../../tsconfig.build.json",
3+
"include": ["./**/*.test-d.ts"]
4+
}

test/core/vite.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ export default defineConfig({
7171
provider: 'istanbul',
7272
reporter: ['text', 'html'],
7373
},
74+
typecheck: {
75+
enabled: true,
76+
tsconfig: './tsconfig.typecheck.json',
77+
},
7478
environmentMatchGlobs: [
7579
['**/*.dom.test.ts', 'happy-dom'],
7680
['test/env-glob-dom/**', 'jsdom'],

0 commit comments

Comments
 (0)