diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index 0993110ee..e29174a36 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -9,6 +9,7 @@ import type { MockInstance } from '@vitest/spy' import type { Constructable } from '@vitest/utils' import type { Formatter } from 'tinyrainbow' +import type { AsymmetricMatcher } from './jest-asymmetric-matchers' import type { diff, getMatcherUtils, stringify } from './jest-matcher-utils' export type ChaiPlugin = Chai.ChaiPlugin @@ -99,8 +100,8 @@ export interface ExpectStatic AsymmetricMatchersContaining { (actual: T, message?: string): Assertion extend: (expects: MatchersObject) => void - anything: () => any - any: (constructor: unknown) => any + anything: () => AsymmetricMatcher + any: (constructor: unknown) => AsymmetricMatcher getState: () => MatcherState setState: (state: Partial) => void not: AsymmetricMatchersContaining @@ -146,7 +147,7 @@ export interface AsymmetricMatchersContaining extends CustomMatcher { * @example * expect({ a: '1', b: 2 }).toEqual(expect.objectContaining({ a: '1' })) */ - objectContaining: (expected: T) => any + objectContaining: (expected: DeeplyAllowMatchers) => any /** * Matches if the received array contains all elements in the expected array. @@ -154,7 +155,7 @@ export interface AsymmetricMatchersContaining extends CustomMatcher { * @example * expect(['a', 'b', 'c']).toEqual(expect.arrayContaining(['b', 'a'])); */ - arrayContaining: (expected: Array) => any + arrayContaining: (expected: Array>) => any /** * Matches if the received string or regex matches the expected pattern. @@ -177,6 +178,14 @@ export interface AsymmetricMatchersContaining extends CustomMatcher { closeTo: (expected: number, precision?: number) => any } +export type DeeplyAllowMatchers = T extends Array + ? (DeeplyAllowMatchers | Element)[] + : T extends object + ? { + [K in keyof T]: DeeplyAllowMatchers | AsymmetricMatcher + } + : T + export interface JestAssertion extends jest.Matchers, CustomMatcher { /** * Used when you want to check that two objects have the same value. @@ -185,7 +194,7 @@ export interface JestAssertion extends jest.Matchers, CustomMa * @example * expect(user).toEqual({ name: 'Alice', age: 30 }); */ - toEqual: (expected: E) => void + toEqual: (expected: DeeplyAllowMatchers) => void /** * Use to test that objects have the same types as well as structure. @@ -193,7 +202,7 @@ export interface JestAssertion extends jest.Matchers, CustomMa * @example * expect(user).toStrictEqual({ name: 'Alice', age: 30 }); */ - toStrictEqual: (expected: E) => void + toStrictEqual: (expected: DeeplyAllowMatchers) => void /** * Checks that a value is what you expect. It calls `Object.is` to compare values. @@ -223,7 +232,7 @@ export interface JestAssertion extends jest.Matchers, CustomMa * address: { city: 'Wonderland' } * }); */ - toMatchObject: (expected: E) => void + toMatchObject: (expected: DeeplyAllowMatchers) => void /** * Used when you want to check that an item is in a list. diff --git a/test/core/test/expect.test-d.ts b/test/core/test/expect.test-d.ts new file mode 100644 index 000000000..1add65213 --- /dev/null +++ b/test/core/test/expect.test-d.ts @@ -0,0 +1,114 @@ +/* eslint-disable no-lone-blocks */ + +import { expect, test } from 'vitest' + +test('expect.* allows asymmetrict mattchers with different types', () => { + // types.ts examples: stringContaining + expect('I have an apple').toEqual(expect.stringContaining('apple')) + expect('I have an apple').toEqual(expect.stringContaining('apple')) + + expect({ a: 'test string' }).toEqual({ a: expect.stringContaining('test') }) + expect({ a: 'test string' }).toEqual<{ a: string }>({ a: expect.stringContaining('test') }) + + // types.ts examples: objectContaining + expect({ a: '1', b: 2 }).toEqual(expect.objectContaining({ a: '1' })) + expect({ a: '1', b: 2 }).toEqual<{ a: string; b: string }>(expect.objectContaining({ a: '1' })) + + // types.ts examples: arrayContaining + expect(['a', 'b', 'c']).toEqual(expect.arrayContaining(['b', 'a'])) + expect(['a', 'b', 'c']).toEqual(expect.arrayContaining(['b', 'a'])) + + // types.ts examples: stringMatching + expect('hello world').toEqual(expect.stringMatching(/^hello/)) + expect('hello world').toEqual(expect.stringMatching(/^hello/)) + + expect('hello world').toEqual(expect.stringMatching('hello')) + expect('hello world').toEqual(expect.stringMatching('hello')) + + // types.ts examples: closeTo + expect(10.45).toEqual(expect.closeTo(10.5, 1)) + expect(10.45).toEqual(expect.closeTo(10.5, 1)) + + expect(5.11).toEqual(expect.closeTo(5.12)) + expect(5.11).toEqual(expect.closeTo(5.12)) + + // expect.any(String) + // https://github.com/vitest-dev/vitest/pull/7016#issuecomment-2517674066 + { + const obj = { + id: '', + name: '', + } + + expect(obj).toEqual({ + id: expect.any(String), + name: 'Amelia', + }) + + expect(obj).toEqual<{ + id: string + name: string + }>({ + id: expect.any(String), + name: 'Amelia', + }) + } + + // expect.any(Date) + // https://github.com/vitest-dev/vitest/issues/4543#issuecomment-1817960296 + // https://github.com/vitest-dev/vitest/issues/4543#issuecomment-1817967628 + // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/62831#issue-1418959169 + { + const actual = {} as { + foo: string + bar: string + createdAt: Date + } + + expect(actual).toEqual({ + foo: 'foo', + bar: 'bar', + createdAt: expect.any(Date), + }) + + expect(actual).toEqual<{ + foo: string + bar: string + createdAt: Date + }>({ + foo: 'foo', + bar: 'bar', + createdAt: expect.any(Date), + }) + + expect(actual).toEqual<{ + foo: string + bar: string + createdAt: Date + }[]>([ + { + foo: 'foo', + bar: 'bar', + createdAt: expect.any(Date), + }, + ]) + } + + // expect.arrayContaining + // https://github.com/jestjs/jest/issues/13812#issue-1555843276 + { + expect([1, 2, 3]).toEqual(expect.arrayContaining(['a'])) + expect([1, 2, 3]).toEqual(expect.arrayContaining(['a'])) + + expect([1, 2, 3]).toEqual(expect.arrayContaining([expect.any(Number)])) + expect([1, 2, 3]).toEqual(expect.arrayContaining([expect.any(Number)])) + + expect([1, 2, 3]).toEqual(expect.arrayContaining([expect.anything()])) + expect([1, 2, 3]).toEqual(expect.arrayContaining([expect.anything()])) + } + + // expect.any(Array) + // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/62831/files#diff-ff7b882e4a29e7fe0e348a6bdf8b11774d606eaa221009b166b01389576d921fR1237 + expect({ list: [1, 2, 3] }).toMatchObject({ list: expect.any(Array) }) + expect({ list: [1, 2, 3] }).toMatchObject<{ list: number[] }>({ list: expect.any(Array) }) +}) diff --git a/test/typescript/test-d/vi.test-d.ts b/test/core/test/vi.test-d.ts similarity index 100% rename from test/typescript/test-d/vi.test-d.ts rename to test/core/test/vi.test-d.ts diff --git a/test/core/tsconfig.typecheck.json b/test/core/tsconfig.typecheck.json new file mode 100644 index 000000000..9f78acb6f --- /dev/null +++ b/test/core/tsconfig.typecheck.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.build.json", + "include": ["./**/*.test-d.ts"] +} diff --git a/test/core/vite.config.ts b/test/core/vite.config.ts index a1bc6fed9..6afeb1693 100644 --- a/test/core/vite.config.ts +++ b/test/core/vite.config.ts @@ -71,6 +71,10 @@ export default defineConfig({ provider: 'istanbul', reporter: ['text', 'html'], }, + typecheck: { + enabled: true, + tsconfig: './tsconfig.typecheck.json', + }, environmentMatchGlobs: [ ['**/*.dom.test.ts', 'happy-dom'], ['test/env-glob-dom/**', 'jsdom'],