diff --git a/jest.config.ts.mjs b/jest.config.ts.mjs index d157b3181005..e4cfdd45f1fa 100644 --- a/jest.config.ts.mjs +++ b/jest.config.ts.mjs @@ -29,9 +29,11 @@ export default { runner: 'jest-runner-tsd', testMatch: [ '**/__typetests__/**/*.test.ts', + '!**/packages/expect/__typetests__/*.test.ts', '!**/packages/expect-utils/__typetests__/*.test.ts', '!**/packages/jest/__typetests__/*.test.ts', '!**/packages/jest-cli/__typetests__/*.test.ts', + '!**/packages/jest-expect/__typetests__/*.test.ts', '!**/packages/jest-mock/__typetests__/*.test.ts', '!**/packages/jest-types/__typetests__/config.test.ts', ], diff --git a/packages/expect/__typetests__/expect.test.ts b/packages/expect/__typetests__/expect.test.ts index 8626b434db55..de0e41683f51 100644 --- a/packages/expect/__typetests__/expect.test.ts +++ b/packages/expect/__typetests__/expect.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {expectAssignable, expectError, expectType} from 'tsd-lite'; +import {describe, expect, test} from 'tstyche'; import type {EqualsFunction} from '@jest/expect-utils'; import { type MatcherContext, @@ -14,94 +14,10 @@ import { type Matchers, type Tester, type TesterContext, - expect, + expect as jestExpect, } from 'expect'; import type * as jestMatcherUtils from 'jest-matcher-utils'; -type M = Matchers; -type N = Matchers; - -expectError(() => { - type E = Matchers; -}); - -const tester1: Tester = function (a, b, customTesters) { - expectType(a); - expectType(b); - expectType>(customTesters); - expectType(this); - expectType(this.equals); - return undefined; -}; - -expectType( - expect.addEqualityTesters([ - tester1, - (a, b, customTesters) => { - expectType(a); - expectType(b); - expectType>(customTesters); - expectType(this); - return true; - }, - function anotherTester(a, b, customTesters) { - expectType(a); - expectType(b); - expectType>(customTesters); - expectType(this); - expectType(this.equals); - return undefined; - }, - ]), -); - -// extend - -type MatcherUtils = typeof jestMatcherUtils & { - iterableEquality: Tester; - subsetEquality: Tester; -}; - -// TODO `actual` should be allowed to have only `unknown` type -expectType( - expect.extend({ - toBeWithinRange(actual: number, floor: number, ceiling: number) { - expectType(this.assertionCalls); - expectType(this.currentTestName); - expectType>(this.customTesters); - expectType<() => void>(this.dontThrow); - expectType(this.error); - expectType(this.equals); - expectType(this.expand); - expectType(this.expectedAssertionsNumber); - expectType(this.expectedAssertionsNumberError); - expectType(this.isExpectingAssertions); - expectType(this.isExpectingAssertionsError); - expectType(this.isNot); - expectType(this.numPassingAsserts); - expectType(this.promise); - expectType>(this.suppressedErrors); - expectType(this.testPath); - expectType(this.utils); - - const pass = actual >= floor && actual <= ceiling; - if (pass) { - return { - message: () => - `expected ${actual} not to be within range ${floor} - ${ceiling}`, - pass: true, - }; - } else { - return { - message: () => - `expected ${actual} to be within range ${floor} - ${ceiling}`, - pass: false, - }; - } - }, - }), -); - declare module 'expect' { interface AsymmetricMatchers { toBeWithinRange(floor: number, ceiling: number): void; @@ -111,141 +27,262 @@ declare module 'expect' { } } -expectType(expect(100).toBeWithinRange(90, 110)); -expectType(expect(101).not.toBeWithinRange(0, 100)); +describe('Matchers', () => { + test('requires between 1 and 2 type arguments', () => { + expect>().type.not.toRaiseError(); + expect>().type.not.toRaiseError(); -expectType( - expect({apples: 6, bananas: 3}).toEqual({ - apples: expect.toBeWithinRange(1, 10), - bananas: expect.not.toBeWithinRange(11, 20), - }), -); + expect().type.toRaiseError( + 'requires between 1 and 2 type arguments', + ); + }); +}); -// MatcherFunction +describe('Expect', () => { + test('.addEqualityTesters()', () => { + const tester1: Tester = function (a, b, customTesters) { + expect(a).type.toBeAny(); + expect(b).type.toBeAny(); + expect(customTesters).type.toEqual>(); + expect(this).type.toEqual(); + expect(this.equals).type.toEqual(); -expectError(() => { - const actualMustBeUnknown: MatcherFunction = (actual: string) => { - return { - message: () => `result: ${actual}`, - pass: actual === 'result', + return undefined; }; - }; -}); -expectError(() => { - const lacksMessage: MatcherFunction = (actual: unknown) => { - return { - pass: actual === 'result', + expect( + jestExpect.addEqualityTesters([ + tester1, + + (a, b, customTesters) => { + expect(a).type.toBeAny(); + expect(b).type.toBeAny(); + expect(customTesters).type.toEqual>(); + expect(this).type.toBeUndefined(); + + return true; + }, + + function anotherTester(a, b, customTesters) { + expect(a).type.toBeAny(); + expect(b).type.toBeAny(); + expect(customTesters).type.toEqual>(); + expect(this).type.toEqual(); + expect(this.equals).type.toEqual(); + + return undefined; + }, + ]), + ).type.toBeVoid(); + }); + + test('.extend()', () => { + type MatcherUtils = typeof jestMatcherUtils & { + iterableEquality: Tester; + subsetEquality: Tester; }; - }; + + expect( + jestExpect.extend({ + // TODO `actual` should be allowed to have only `unknown` type + toBeWithinRange(actual: number, floor: number, ceiling: number) { + expect(this.assertionCalls).type.toBeNumber(); + expect(this.currentTestName).type.toEqual(); + expect(this.customTesters).type.toEqual>(); + expect(this.dontThrow).type.toEqual<() => void>(); + expect(this.error).type.toEqual(); + expect(this.equals).type.toEqual(); + expect(this.expand).type.toEqual(); + expect(this.expectedAssertionsNumber).type.toEqual(); + expect(this.expectedAssertionsNumberError).type.toEqual< + Error | undefined + >(); + expect(this.isExpectingAssertions).type.toBeBoolean(); + expect(this.isExpectingAssertionsError).type.toEqual< + Error | undefined + >(); + expect(this.isNot).type.toEqual(); + expect(this.numPassingAsserts).type.toBeNumber(); + expect(this.promise).type.toEqual(); + expect(this.suppressedErrors).type.toEqual>(); + expect(this.testPath).type.toEqual(); + expect(this.utils).type.toEqual(); + + const pass = actual >= floor && actual <= ceiling; + if (pass) { + return { + message: () => + `expected ${actual} not to be within range ${floor} - ${ceiling}`, + pass: true, + }; + } else { + return { + message: () => + `expected ${actual} to be within range ${floor} - ${ceiling}`, + pass: false, + }; + } + }, + }), + ).type.toBeVoid(); + + expect(jestExpect(100).toBeWithinRange(90, 110)).type.toBeVoid(); + expect(jestExpect(101).not.toBeWithinRange(0, 100)).type.toBeVoid(); + + expect( + jestExpect({apples: 6, bananas: 3}).toEqual({ + apples: jestExpect.toBeWithinRange(1, 10), + bananas: jestExpect.not.toBeWithinRange(11, 20), + }), + ).type.toBeVoid(); + }); + + test('does not define the `.toMatchSnapshot()` matcher', () => { + expect(jestExpect(null)).type.not.toHaveProperty('toMatchSnapshot'); + }); }); -expectError(() => { - const lacksPass: MatcherFunction = (actual: unknown) => { - return { - message: () => `result: ${actual}`, +describe('MatcherFunction', () => { + test('models typings of a matcher function', () => { + type ToBeWithinRange = ( + this: MatcherContext, + actual: unknown, + floor: number, + ceiling: number, + ) => any; + + const toBeWithinRange: MatcherFunction<[floor: number, ceiling: number]> = ( + actual: unknown, + floor: unknown, + ceiling: unknown, + ) => { + return { + message: () => `actual ${actual}; range ${floor}-${ceiling}`, + pass: true, + }; }; - }; -}); -type ToBeWithinRange = ( - this: MatcherContext, - actual: unknown, - floor: number, - ceiling: number, -) => any; - -const toBeWithinRange: MatcherFunction<[floor: number, ceiling: number]> = ( - actual: unknown, - floor: unknown, - ceiling: unknown, -) => { - return { - message: () => `actual ${actual}; range ${floor}-${ceiling}`, - pass: true, - }; -}; - -expectAssignable(toBeWithinRange); - -type AllowOmittingExpected = (this: MatcherContext, actual: unknown) => any; - -const allowOmittingExpected: MatcherFunction = ( - actual: unknown, - ...expect: Array -) => { - if (expect.length > 0) { - throw new Error('This matcher does not take any expected argument.'); - } + expect().type.toBeAssignable(toBeWithinRange); + }); - return { - message: () => `actual ${actual}`, - pass: true, - }; -}; + test('requires the `actual` argument to be of type `unknown`', () => { + const actualMustBeUnknown = (actual: string) => { + return { + message: () => `result: ${actual}`, + pass: actual === 'result', + }; + }; -expectAssignable(allowOmittingExpected); + expect().type.not.toBeAssignable(actualMustBeUnknown); + }); -// MatcherContext + test('allows omitting the `expected` argument', () => { + type AllowOmittingExpected = (this: MatcherContext, actual: unknown) => any; -const toHaveContext: MatcherFunction = function ( - actual: unknown, - ...expect: Array -) { - expectType(this); + const allowOmittingExpected: MatcherFunction = ( + actual: unknown, + ...expected: Array + ) => { + if (expected.length > 0) { + throw new Error('This matcher does not take any expected argument.'); + } - if (expect.length > 0) { - throw new Error('This matcher does not take any expected argument.'); - } + return { + message: () => `actual ${actual}`, + pass: true, + }; + }; - return { - message: () => `result: ${actual}`, - pass: actual === 'result', - }; -}; + expect().type.toBeAssignable(allowOmittingExpected); + }); -interface CustomContext extends MatcherContext { - customMethod(): void; -} + test('the `message` property is required in the return type', () => { + const lacksMessage = (actual: unknown) => { + return { + pass: actual === 'result', + }; + }; -const customContext: MatcherFunctionWithContext = function ( - actual: unknown, - ...expect: Array -) { - expectType(this); - expectType(this.customMethod()); + expect().type.not.toBeAssignable(lacksMessage); + }); - if (expect.length > 0) { - throw new Error('This matcher does not take any expected argument.'); - } + test('the `pass` property is required in the return type', () => { + const lacksPass = (actual: unknown) => { + return { + message: () => `result: ${actual}`, + }; + }; + + expect().type.not.toBeAssignable(lacksPass); + }); + + test('context is defined inside a matcher function', () => { + const toHaveContext: MatcherFunction = function ( + actual: unknown, + ...expected: Array + ) { + expect(this).type.toEqual(); + + if (expected.length > 0) { + throw new Error('This matcher does not take any expected argument.'); + } + + return { + message: () => `result: ${actual}`, + pass: actual === 'result', + }; + }; + }); + + test('context can be customized', () => { + interface CustomContext extends MatcherContext { + customMethod(): void; + } + + const customContext: MatcherFunctionWithContext = function ( + actual: unknown, + ...expected: Array + ) { + expect(this).type.toEqual(); + expect(this.customMethod()).type.toBeVoid(); + + if (expected.length > 0) { + throw new Error('This matcher does not take any expected argument.'); + } + + return { + message: () => `result: ${actual}`, + pass: actual === 'result', + }; + }; + }); + + test('context and type of `expected` can be customized', () => { + interface CustomContext extends MatcherContext { + customMethod(): void; + } + + type CustomStateAndExpected = ( + this: CustomContext, + actual: unknown, + count: number, + ) => any; + + const customContextAndExpected: MatcherFunctionWithContext< + CustomContext, + [count: number] + > = function (actual: unknown, count: unknown) { + expect(this).type.toEqual(); + expect(this.customMethod()).type.toBeVoid(); + + return { + message: () => `count: ${count}`, + pass: actual === count, + }; + }; - return { - message: () => `result: ${actual}`, - pass: actual === 'result', - }; -}; - -type CustomStateAndExpected = ( - this: CustomContext, - actual: unknown, - count: number, -) => any; - -const customStateAndExpected: MatcherFunctionWithContext< - CustomContext, - [count: number] -> = function (actual: unknown, count: unknown) { - expectType(this); - expectType(this.customMethod()); - - return { - message: () => `count: ${count}`, - pass: actual === count, - }; -}; - -expectAssignable(customStateAndExpected); - -expectError(() => { - expect({}).toMatchSnapshot(); + expect().type.toBeAssignable( + customContextAndExpected, + ); + }); }); diff --git a/packages/expect/__typetests__/expectTyped.test.ts b/packages/expect/__typetests__/expectTyped.test.ts index 155431a611b1..0316fa24d5d1 100644 --- a/packages/expect/__typetests__/expectTyped.test.ts +++ b/packages/expect/__typetests__/expectTyped.test.ts @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import {expectAssignable, expectError, expectType} from 'tsd-lite'; -import {Matchers, expect} from 'expect'; +import {describe, expect, test} from 'tstyche'; +import {expect as _expect} from 'expect'; declare module 'expect' { interface Matchers { @@ -14,9 +14,13 @@ declare module 'expect' { } } -expectType(expect(100).toTypedEqual(100)); -expectType(expect(101).not.toTypedEqual(101)); +describe('Expect', () => { + test('allows type inference of the `actual` argument', () => { + expect(_expect(100).toTypedEqual(100)).type.toBeVoid(); + expect(_expect(101).not.toTypedEqual(100)).type.toBeVoid(); -expectError(() => { - expect(100).toTypedEqual('three'); + expect(_expect(100).toTypedEqual('three')).type.toRaiseError( + "Argument of type 'string' is not assignable to parameter of type 'number'.", + ); + }); }); diff --git a/packages/jest-expect/__typetests__/jest-expect.test.ts b/packages/jest-expect/__typetests__/jest-expect.test.ts index b576aa2e2cbd..966ac9e9fdd9 100644 --- a/packages/jest-expect/__typetests__/jest-expect.test.ts +++ b/packages/jest-expect/__typetests__/jest-expect.test.ts @@ -5,30 +5,9 @@ * LICENSE file in the root directory of this source tree. */ -import { - expectAssignable, - expectError, - expectNotAssignable, - expectType, -} from 'tsd-lite'; +import {describe, expect, test} from 'tstyche'; import {jestExpect} from '@jest/expect'; -import {expect} from 'expect'; -import type {Plugin} from 'pretty-format'; - -expectType(jestExpect({}).toMatchSnapshot()); - -expectError(() => { - expect({}).toMatchSnapshot(); -}); - -expectType(jestExpect.addSnapshotSerializer({} as Plugin)); - -expectError(() => { - expect.addSnapshotSerializer(); -}); - -expectAssignable(jestExpect); -expectNotAssignable(expect); +import {expect as _expect} from 'expect'; declare module 'expect' { interface Matchers { @@ -36,9 +15,31 @@ declare module 'expect' { } } -expectType(jestExpect(100).toTypedEqual(100)); -expectType(jestExpect(101).not.toTypedEqual(101)); +describe('JestExpect', () => { + test('defines the `.toMatchSnapshot()` matcher', () => { + expect(jestExpect(null)).type.toHaveProperty('toMatchSnapshot'); + + expect(_expect(null)).type.not.toHaveProperty('toMatchSnapshot'); + }); + + test('defines the `.addSnapshotSerializer()` method', () => { + expect(jestExpect).type.toHaveProperty('addSnapshotSerializer'); + + expect(_expect).type.not.toHaveProperty('addSnapshotSerializer'); + }); + + test('is superset of `Expect`', () => { + expect().type.toMatch(); + + expect().type.not.toMatch(); + }); + + test('allows type inference of the `actual` argument', () => { + expect(jestExpect(100).toTypedEqual(100)).type.toBeVoid(); + expect(jestExpect(101).not.toTypedEqual(100)).type.toBeVoid(); -expectError(() => { - jestExpect(100).toTypedEqual('three'); + expect(jestExpect(100).toTypedEqual('three')).type.toRaiseError( + "Argument of type 'string' is not assignable to parameter of type 'number'.", + ); + }); }); diff --git a/tstyche.config.json b/tstyche.config.json index 8defe3d1a011..53554e719646 100644 --- a/tstyche.config.json +++ b/tstyche.config.json @@ -1,8 +1,10 @@ { "testFileMatch": [ + "**/packages/expect/__typetests__/*.test.ts", "**/packages/expect-utils/__typetests__/*.test.ts", "**/packages/jest/__typetests__/*.test.ts", "**/packages/jest-cli/__typetests__/*.test.ts", + "**/packages/jest-expect/__typetests__/*.test.ts", "**/packages/jest-mock/__typetests__/*.test.ts", "**/packages/jest-types/__typetests__/config.test.ts" ]