From 3d31e39601859faa1ce9f7d9e907235446c84e04 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 12 Feb 2022 15:32:54 +0200 Subject: [PATCH 01/12] fix: expose ExpectationResult --- examples/expect-extend/toBeWithinRange.ts | 6 +- packages/expect/src/index.ts | 2 +- packages/expect/src/types.ts | 2 +- packages/jest-jasmine2/src/jestExpect.ts | 16 ++--- packages/jest-jasmine2/src/types.ts | 6 +- packages/jest-snapshot/src/index.ts | 87 ++++++++++++----------- 6 files changed, 59 insertions(+), 60 deletions(-) diff --git a/examples/expect-extend/toBeWithinRange.ts b/examples/expect-extend/toBeWithinRange.ts index b7d43cddec73..e06e0d5bb63f 100644 --- a/examples/expect-extend/toBeWithinRange.ts +++ b/examples/expect-extend/toBeWithinRange.ts @@ -6,13 +6,13 @@ */ import {expect} from '@jest/globals'; -import type {RawMatcherFn} from 'expect'; +import type {ExpectationResult} from 'expect'; -const toBeWithinRange: RawMatcherFn = ( +const toBeWithinRange = ( actual: number, floor: number, ceiling: number, -) => { +): ExpectationResult => { const pass = actual >= floor && actual <= ceiling; if (pass) { return { diff --git a/packages/expect/src/index.ts b/packages/expect/src/index.ts index 1be8f83c9c0c..66f88b5c08b2 100644 --- a/packages/expect/src/index.ts +++ b/packages/expect/src/index.ts @@ -52,9 +52,9 @@ import type { export type { AsymmetricMatchers, Expect, + ExpectationResult, MatcherState, Matchers, - RawMatcherFn, } from './types'; export class JestAssertionError extends Error { diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index 9e037c42d9f0..e53f09493335 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -21,7 +21,7 @@ export type AsyncExpectationResult = Promise; export type ExpectationResult = SyncExpectationResult | AsyncExpectationResult; export type RawMatcherFn = { - (this: T, actual: any, expected: any, options?: any): ExpectationResult; + (this: T, actual: any, ...expected: Array): ExpectationResult; /** @internal */ [INTERNAL_MATCHER_FLAG]?: boolean; }; diff --git a/packages/jest-jasmine2/src/jestExpect.ts b/packages/jest-jasmine2/src/jestExpect.ts index 836de548e832..579b6a7de605 100644 --- a/packages/jest-jasmine2/src/jestExpect.ts +++ b/packages/jest-jasmine2/src/jestExpect.ts @@ -7,7 +7,7 @@ /* eslint-disable local/prefer-spread-eventually */ -import {type MatcherState, type RawMatcherFn, expect} from 'expect'; +import {type MatcherState, expect} from 'expect'; import { addSerializer, toMatchInlineSnapshot, @@ -41,23 +41,15 @@ export default function jestExpect(config: {expand: boolean}): void { jestMatchersObject[name] = function ( this: MatcherState, ...args: Array - ): RawMatcherFn { + ) { // use "expect.extend" if you need to use equality testers (via this.equal) const result = jasmineMatchersObject[name](null, null); // if there is no 'negativeCompare', both should be handled by `compare` const negativeCompare = result.negativeCompare || result.compare; return this.isNot - ? negativeCompare.apply( - null, - // @ts-expect-error - args, - ) - : result.compare.apply( - null, - // @ts-expect-error - args, - ); + ? negativeCompare.apply(null, args) + : result.compare.apply(null, args); }; }); diff --git a/packages/jest-jasmine2/src/types.ts b/packages/jest-jasmine2/src/types.ts index 34ef38ebddec..750a43f9b13a 100644 --- a/packages/jest-jasmine2/src/types.ts +++ b/packages/jest-jasmine2/src/types.ts @@ -7,7 +7,7 @@ import type {AssertionError} from 'assert'; import type {Config} from '@jest/types'; -import type {Expect, RawMatcherFn} from 'expect'; +import type {Expect, ExpectationResult} from 'expect'; import type CallTracker from './jasmine/CallTracker'; import type Env from './jasmine/Env'; import type JsApiReporter from './jasmine/JsApiReporter'; @@ -48,8 +48,8 @@ export interface Spy extends Record { type JasmineMatcher = { (matchersUtil: unknown, context: unknown): JasmineMatcher; - compare: () => RawMatcherFn; - negativeCompare: () => RawMatcherFn; + compare(...args: Array): ExpectationResult; + negativeCompare(...args: Array): ExpectationResult; }; export type JasmineMatchersObject = {[id: string]: JasmineMatcher}; diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index fd3b7f73fe2f..ad158dd6d1e2 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -7,7 +7,7 @@ import * as fs from 'graceful-fs'; import type {Config} from '@jest/types'; -import type {RawMatcherFn} from 'expect'; +import type {ExpectationResult} from 'expect'; import type {FS as HasteFS} from 'jest-haste-map'; import { BOLD_WEIGHT, @@ -156,11 +156,12 @@ export const cleanup = ( }; }; -export const toMatchSnapshot: RawMatcherFn = function ( +export const toMatchSnapshot = function ( + this: Context, received: unknown, propertiesOrHint?: object | Config.Path, hint?: Config.Path, -) { +): ExpectationResult { const matcherName = 'toMatchSnapshot'; let properties; @@ -214,11 +215,12 @@ export const toMatchSnapshot: RawMatcherFn = function ( }); }; -export const toMatchInlineSnapshot: RawMatcherFn = function ( +export const toMatchInlineSnapshot = function ( + this: Context, received: unknown, propertiesOrSnapshot?: object | string, inlineSnapshot?: string, -) { +): ExpectationResult { const matcherName = 'toMatchInlineSnapshot'; let properties; @@ -407,11 +409,12 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => { }; }; -export const toThrowErrorMatchingSnapshot: RawMatcherFn = function ( +export const toThrowErrorMatchingSnapshot = function ( + this: Context, received: unknown, - hint: string | undefined, // because error TS1016 for hint?: string - fromPromise: boolean, -) { + hint?: string, + fromPromise?: boolean, +): ExpectationResult { const matcherName = 'toThrowErrorMatchingSnapshot'; // Future breaking change: Snapshot hint must be a string @@ -429,40 +432,44 @@ export const toThrowErrorMatchingSnapshot: RawMatcherFn = function ( ); }; -export const toThrowErrorMatchingInlineSnapshot: RawMatcherFn = - function (received: unknown, inlineSnapshot?: string, fromPromise?: boolean) { - const matcherName = 'toThrowErrorMatchingInlineSnapshot'; - - if (inlineSnapshot !== undefined && typeof inlineSnapshot !== 'string') { - const options: MatcherHintOptions = { - expectedColor: noColor, - isNot: this.isNot, - promise: this.promise, - }; +export const toThrowErrorMatchingInlineSnapshot = function ( + this: Context, + received: unknown, + inlineSnapshot?: string, + fromPromise?: boolean, +): ExpectationResult { + const matcherName = 'toThrowErrorMatchingInlineSnapshot'; - throw new Error( - matcherErrorMessage( - matcherHint(matcherName, undefined, SNAPSHOT_ARG, options), - 'Inline snapshot must be a string', - printWithType('Inline snapshot', inlineSnapshot, serialize), - ), - ); - } + if (inlineSnapshot !== undefined && typeof inlineSnapshot !== 'string') { + const options: MatcherHintOptions = { + expectedColor: noColor, + isNot: this.isNot, + promise: this.promise, + }; - return _toThrowErrorMatchingSnapshot( - { - context: this, - inlineSnapshot: - inlineSnapshot !== undefined - ? stripAddedIndentation(inlineSnapshot) - : undefined, - isInline: true, - matcherName, - received, - }, - fromPromise, + throw new Error( + matcherErrorMessage( + matcherHint(matcherName, undefined, SNAPSHOT_ARG, options), + 'Inline snapshot must be a string', + printWithType('Inline snapshot', inlineSnapshot, serialize), + ), ); - }; + } + + return _toThrowErrorMatchingSnapshot( + { + context: this, + inlineSnapshot: + inlineSnapshot !== undefined + ? stripAddedIndentation(inlineSnapshot) + : undefined, + isInline: true, + matcherName, + received, + }, + fromPromise, + ); +}; const _toThrowErrorMatchingSnapshot = ( config: MatchSnapshotConfig, From 2273adf6c26ca14c499adb9fb736c0e50f596b1b Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 12 Feb 2022 17:01:21 +0200 Subject: [PATCH 02/12] upgrade tsd-lite --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7ed274724ad5..d5a75efc0db4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21077,11 +21077,11 @@ __metadata: linkType: hard "tsd-lite@npm:^0.5.0, tsd-lite@npm:^0.5.1": - version: 0.5.2 - resolution: "tsd-lite@npm:0.5.2" + version: 0.5.3 + resolution: "tsd-lite@npm:0.5.3" peerDependencies: "@tsd/typescript": ^3.8.3 || ^4.0.7 - checksum: e3e49a4b660149bc2056154db0b1f42d636826e9b7a1c59f4790efd6f5b3d158ab2b9c996bf4362688b41b479d9ae99ec19b09dc35211c07ccb0d0f41f006a0b + checksum: b1a5ffcd584b66e34173f0182d4cc7b2c5a145655346f9014318dd16a7ab3418d462df1cfa13923f79628e7900e5b8992c1210f59492adc9b7ef242b2026ab14 languageName: node linkType: hard From 47bca7f9331c0b3a157b117f7832380d045a8a90 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 12 Feb 2022 17:02:05 +0200 Subject: [PATCH 03/12] add test for ExpectationResult --- packages/expect/__typetests__/expect.test.ts | 29 +++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/expect/__typetests__/expect.test.ts b/packages/expect/__typetests__/expect.test.ts index acd40562f0a0..514a6009350e 100644 --- a/packages/expect/__typetests__/expect.test.ts +++ b/packages/expect/__typetests__/expect.test.ts @@ -7,7 +7,7 @@ import {expectError, expectType} from 'tsd-lite'; import type {EqualsFunction, Tester} from '@jest/expect-utils'; -import {type Matchers, expect} from 'expect'; +import {type ExpectationResult, type Matchers, expect} from 'expect'; import type * as jestMatcherUtils from 'jest-matcher-utils'; type M = Matchers; @@ -79,3 +79,30 @@ expectType( bananas: expect.not.toBeWithinRange(11, 20), }), ); + +const toBeResult = (received: string): ExpectationResult => { + if (received === 'result') { + return { + message: () => 'is result', + pass: true, + }; + } else { + return { + message: () => 'is not result', + pass: false, + }; + } +}; + +expectType(expect.extend({toBeResult})); + +expectError(() => { + const lacksElseBranch = (received: string): ExpectationResult => { + if (received === 'result') { + return { + message: () => 'is result', + pass: true, + }; + } + }; +}); From 3064a7847cd87808e08d29bf99edc4785a04cbcc Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 12 Feb 2022 17:09:07 +0200 Subject: [PATCH 04/12] one more type test --- packages/expect/__typetests__/expect.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/expect/__typetests__/expect.test.ts b/packages/expect/__typetests__/expect.test.ts index 514a6009350e..fc5245d5b0d0 100644 --- a/packages/expect/__typetests__/expect.test.ts +++ b/packages/expect/__typetests__/expect.test.ts @@ -80,6 +80,8 @@ expectType( }), ); +// ExpectationResult + const toBeResult = (received: string): ExpectationResult => { if (received === 'result') { return { @@ -106,3 +108,17 @@ expectError(() => { } }; }); + +expectError(() => { + const lacksMessage = (received: string): ExpectationResult => { + if (received === 'result') { + return { + pass: true, + }; + } else { + return { + pass: false, + }; + } + }; +}); From eafcbc25696a8b377afe8398cb7d1ea8ae06e523 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 12 Feb 2022 17:34:05 +0200 Subject: [PATCH 05/12] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c74f57649f92..d26af022cd43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - `[expect]` Move typings of `.not`, `.rejects` and `.resolves` modifiers outside of `Matchers` interface ([#12346](https://github.com/facebook/jest/pull/12346)) - `[expect]` Expose `AsymmetricMatchers` and `RawMatcherFn` interfaces ([#12363](https://github.com/facebook/jest/pull/12363)) +- `[expect]` Expose `ExpectationResult` type instead of `RawMatcherFn` type (partly reverts #12363) ([#12376](https://github.com/facebook/jest/pull/12376)) - `[jest-environment-jsdom]` Make `jsdom` accessible to extending environments again ([#12232](https://github.com/facebook/jest/pull/12232)) - `[jest-jasmine2, jest-types]` [**BREAKING**] Move all `jasmine` specific types from `@jest/types` to its own package ([#12125](https://github.com/facebook/jest/pull/12125)) From f97e5a81822ec7144df56b693c21a4bd3944c014 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sun, 13 Feb 2022 07:15:44 +0200 Subject: [PATCH 06/12] add MatcherState test --- packages/expect/__typetests__/expect.test.ts | 42 +++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/expect/__typetests__/expect.test.ts b/packages/expect/__typetests__/expect.test.ts index fc5245d5b0d0..3db1270ee699 100644 --- a/packages/expect/__typetests__/expect.test.ts +++ b/packages/expect/__typetests__/expect.test.ts @@ -7,7 +7,12 @@ import {expectError, expectType} from 'tsd-lite'; import type {EqualsFunction, Tester} from '@jest/expect-utils'; -import {type ExpectationResult, type Matchers, expect} from 'expect'; +import { + type ExpectationResult, + type MatcherState, + type Matchers, + expect, +} from 'expect'; import type * as jestMatcherUtils from 'jest-matcher-utils'; type M = Matchers; @@ -122,3 +127,38 @@ expectError(() => { } }; }); + +// MatcherState + +function toHaveContext( + this: MatcherState, + received: string, +): ExpectationResult { + expectType(this.assertionCalls); + expectType(this.currentTestName); + expectType<(() => void) | undefined>(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.promise); + expectType>(this.suppressedErrors); + expectType(this.testPath); + expectType(this.utils); + + if (received === 'result') { + return { + message: () => 'is result', + pass: true, + }; + } else { + return { + message: () => 'is not result', + pass: false, + }; + } +} From f0407fc41e1d3c0cf70719e211e69d87e4b44b38 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sun, 13 Feb 2022 11:14:22 +0200 Subject: [PATCH 07/12] fix changelog --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d26af022cd43..8caf3e93eb1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,7 @@ ### Fixes - `[expect]` Move typings of `.not`, `.rejects` and `.resolves` modifiers outside of `Matchers` interface ([#12346](https://github.com/facebook/jest/pull/12346)) -- `[expect]` Expose `AsymmetricMatchers` and `RawMatcherFn` interfaces ([#12363](https://github.com/facebook/jest/pull/12363)) -- `[expect]` Expose `ExpectationResult` type instead of `RawMatcherFn` type (partly reverts #12363) ([#12376](https://github.com/facebook/jest/pull/12376)) +- `[expect]` Expose `AsymmetricMatchers` and `ExpectationResult` interfaces ([#12363](https://github.com/facebook/jest/pull/12363), [#12376](https://github.com/facebook/jest/pull/12376)) - `[jest-environment-jsdom]` Make `jsdom` accessible to extending environments again ([#12232](https://github.com/facebook/jest/pull/12232)) - `[jest-jasmine2, jest-types]` [**BREAKING**] Move all `jasmine` specific types from `@jest/types` to its own package ([#12125](https://github.com/facebook/jest/pull/12125)) From b16b3a4bf34bccb6daf3f5796737e89fb186971c Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Mon, 14 Feb 2022 10:24:01 +0200 Subject: [PATCH 08/12] expose MatcherFunction --- packages/expect/__typetests__/expect.test.ts | 158 +++++++++++-------- packages/expect/src/index.ts | 2 + packages/expect/src/types.ts | 17 +- packages/jest-snapshot/src/types.ts | 4 +- 4 files changed, 115 insertions(+), 66 deletions(-) diff --git a/packages/expect/__typetests__/expect.test.ts b/packages/expect/__typetests__/expect.test.ts index 3db1270ee699..122f92985d09 100644 --- a/packages/expect/__typetests__/expect.test.ts +++ b/packages/expect/__typetests__/expect.test.ts @@ -9,6 +9,8 @@ import {expectError, expectType} from 'tsd-lite'; import type {EqualsFunction, Tester} from '@jest/expect-utils'; import { type ExpectationResult, + type MatcherFunction, + type MatcherFunctionWithContext, type MatcherState, type Matchers, expect, @@ -29,6 +31,7 @@ type MatcherUtils = typeof jestMatcherUtils & { subsetEquality: Tester; }; +// TODO `actual` should be allowed to have only `unknown` type expectType( expect.extend({ toBeWithinRange(actual: number, floor: number, ceiling: number) { @@ -85,80 +88,111 @@ expectType( }), ); -// ExpectationResult +// MatcherFunction -const toBeResult = (received: string): ExpectationResult => { - if (received === 'result') { - return { - message: () => 'is result', - pass: true, - }; - } else { +expectError(() => { + const actualMustBeUnknown: MatcherFunction = (actual: string) => { return { - message: () => 'is not result', - pass: false, + message: () => `result: ${actual}`, + pass: actual === 'result', }; - } -}; - -expectType(expect.extend({toBeResult})); + }; +}); expectError(() => { - const lacksElseBranch = (received: string): ExpectationResult => { - if (received === 'result') { - return { - message: () => 'is result', - pass: true, - }; - } + const lacksMessage: MatcherFunction = (actual: unknown) => { + return { + pass: actual === 'result', + }; }; }); expectError(() => { - const lacksMessage = (received: string): ExpectationResult => { - if (received === 'result') { - return { - pass: true, - }; - } else { - return { - pass: false, - }; - } + const lacksPass: MatcherFunction = (actual: unknown) => { + return { + message: () => `result: ${actual}`, + }; }; }); -// MatcherState +type ToBeWithinRange = ( + this: MatcherState, + actual: unknown, + floor: number, + ceiling: number, +) => ExpectationResult; + +const toBeWithinRange: MatcherFunction<[floor: number, ceiling: number]> = ( + actual: unknown, + floor: unknown, + ceiling: unknown, +) => { + return { + message: () => `actual ${actual}; range ${floor}-${ceiling}`, + pass: true, + }; +}; -function toHaveContext( +expectType(toBeWithinRange); + +type AllowOmittingExpected = ( this: MatcherState, - received: string, -): ExpectationResult { - expectType(this.assertionCalls); - expectType(this.currentTestName); - expectType<(() => void) | undefined>(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.promise); - expectType>(this.suppressedErrors); - expectType(this.testPath); - expectType(this.utils); - - if (received === 'result') { - return { - message: () => 'is result', - pass: true, - }; - } else { - return { - message: () => 'is not result', - pass: false, - }; - } + actual: unknown, +) => ExpectationResult; + +const allowOmittingExpected: MatcherFunction = (actual: unknown) => { + return { + message: () => `actual ${actual}`, + pass: true, + }; +}; + +expectType(allowOmittingExpected); + +// MatcherState + +const toHaveContext: MatcherFunction = function (actual: unknown) { + expectType(this); + + return { + message: () => `result: ${actual}`, + pass: actual === 'result', + }; +}; + +interface CustomContext extends MatcherState { + customMethod(): void; } + +const customContext: MatcherFunctionWithContext = function ( + actual: unknown, +) { + expectType(this); + expectType(this.customMethod()); + + return { + message: () => `result: ${actual}`, + pass: actual === 'result', + }; +}; + +type CustomContextAndExpected = ( + this: CustomContext, + actual: unknown, + count: number, +) => ExpectationResult; + +const customContextAndExpected: MatcherFunctionWithContext< + CustomContext, + [count: number] +> = function (actual: unknown, count: unknown) { + expectType(this); + expectType(this.customMethod()); + + return { + message: () => `count: ${count}`, + pass: actual === count, + }; +}; + +expectType(customContextAndExpected); diff --git a/packages/expect/src/index.ts b/packages/expect/src/index.ts index 66f88b5c08b2..ac6a2d9db36b 100644 --- a/packages/expect/src/index.ts +++ b/packages/expect/src/index.ts @@ -53,6 +53,8 @@ export type { AsymmetricMatchers, Expect, ExpectationResult, + MatcherFunction, + MatcherFunctionWithContext, MatcherState, Matchers, } from './types'; diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index e53f09493335..232c970ab759 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -20,6 +20,19 @@ export type AsyncExpectationResult = Promise; export type ExpectationResult = SyncExpectationResult | AsyncExpectationResult; +export type MatcherFunctionWithContext< + Context extends MatcherState = MatcherState, + Expected extends Array = [], +> = ( + this: Context, + actual: unknown, + ...expected: Expected +) => ExpectationResult; + +export type MatcherFunction = []> = + MatcherFunctionWithContext; + +// TODO should be replaced with `MatcherFunctionWithContext` export type RawMatcherFn = { (this: T, actual: any, ...expected: Array): ExpectationResult; /** @internal */ @@ -29,7 +42,7 @@ export type RawMatcherFn = { export type ThrowingMatcherFn = (actual: any) => void; export type PromiseMatcherFn = (actual: any) => Promise; -export type MatcherState = { +export interface MatcherState { assertionCalls: number; currentTestName?: string; dontThrow?(): void; @@ -48,7 +61,7 @@ export type MatcherState = { iterableEquality: Tester; subsetEquality: Tester; }; -}; +} export interface AsymmetricMatcher { asymmetricMatch(other: unknown): boolean; diff --git a/packages/jest-snapshot/src/types.ts b/packages/jest-snapshot/src/types.ts index dcc0c8e12de5..0013a08d081f 100644 --- a/packages/jest-snapshot/src/types.ts +++ b/packages/jest-snapshot/src/types.ts @@ -8,9 +8,9 @@ import type {MatcherState} from 'expect'; import type SnapshotState from './State'; -export type Context = MatcherState & { +export interface Context extends MatcherState { snapshotState: SnapshotState; -}; +} export type MatchSnapshotConfig = { context: Context; From 71d8de10da00916f3dcbd683caef6380b4d711e9 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Tue, 15 Feb 2022 20:06:15 +0200 Subject: [PATCH 09/12] fix example --- examples/expect-extend/toBeWithinRange.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/examples/expect-extend/toBeWithinRange.ts b/examples/expect-extend/toBeWithinRange.ts index e06e0d5bb63f..55ce710f440c 100644 --- a/examples/expect-extend/toBeWithinRange.ts +++ b/examples/expect-extend/toBeWithinRange.ts @@ -6,13 +6,21 @@ */ import {expect} from '@jest/globals'; -import type {ExpectationResult} from 'expect'; +import type {MatcherFunction} from 'expect'; + +const toBeWithinRange: MatcherFunction<[floor: number, ceiling: number]> = ( + actual: unknown, + floor: unknown, + ceiling: unknown, +) => { + if ( + typeof actual !== 'number' || + typeof floor !== 'number' || + typeof ceiling !== 'number' + ) { + throw new Error('These must be of type number!'); + } -const toBeWithinRange = ( - actual: number, - floor: number, - ceiling: number, -): ExpectationResult => { const pass = actual >= floor && actual <= ceiling; if (pass) { return { From ab8c2eaebce89b115f6c52950fab1b47fd030314 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Tue, 15 Feb 2022 21:11:24 +0200 Subject: [PATCH 10/12] fix last details --- packages/expect/__typetests__/expect.test.ts | 57 +++-- packages/expect/src/index.ts | 3 +- packages/expect/src/types.ts | 36 ++- packages/jest-jasmine2/src/types.ts | 6 +- packages/jest-snapshot/src/index.ts | 225 +++++++++---------- 5 files changed, 165 insertions(+), 162 deletions(-) diff --git a/packages/expect/__typetests__/expect.test.ts b/packages/expect/__typetests__/expect.test.ts index 122f92985d09..4ea6b99d31e9 100644 --- a/packages/expect/__typetests__/expect.test.ts +++ b/packages/expect/__typetests__/expect.test.ts @@ -5,12 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -import {expectError, expectType} from 'tsd-lite'; +import {expectAssignable, expectError, expectType} from 'tsd-lite'; import type {EqualsFunction, Tester} from '@jest/expect-utils'; import { - type ExpectationResult, type MatcherFunction, - type MatcherFunctionWithContext, + type MatcherFunctionWithState, type MatcherState, type Matchers, expect, @@ -120,7 +119,7 @@ type ToBeWithinRange = ( actual: unknown, floor: number, ceiling: number, -) => ExpectationResult; +) => any; const toBeWithinRange: MatcherFunction<[floor: number, ceiling: number]> = ( actual: unknown, @@ -133,60 +132,76 @@ const toBeWithinRange: MatcherFunction<[floor: number, ceiling: number]> = ( }; }; -expectType(toBeWithinRange); +expectAssignable(toBeWithinRange); -type AllowOmittingExpected = ( - this: MatcherState, +type AllowOmittingExpected = (this: MatcherState, actual: unknown) => any; + +const allowOmittingExpected: MatcherFunction<[]> = ( actual: unknown, -) => ExpectationResult; + ...expect: Array +) => { + if (expect.length !== 0) { + throw new Error('This matcher does not take any expected argument.'); + } -const allowOmittingExpected: MatcherFunction = (actual: unknown) => { return { message: () => `actual ${actual}`, pass: true, }; }; -expectType(allowOmittingExpected); +expectAssignable(allowOmittingExpected); // MatcherState -const toHaveContext: MatcherFunction = function (actual: unknown) { +const toHaveContext: MatcherFunction = function ( + actual: unknown, + ...expect: Array +) { expectType(this); + if (expect.length !== 0) { + throw new Error('This matcher does not take any expected argument.'); + } + return { message: () => `result: ${actual}`, pass: actual === 'result', }; }; -interface CustomContext extends MatcherState { +interface CustomState extends MatcherState { customMethod(): void; } -const customContext: MatcherFunctionWithContext = function ( +const customContext: MatcherFunctionWithState = function ( actual: unknown, + ...expect: Array ) { - expectType(this); + expectType(this); expectType(this.customMethod()); + if (expect.length !== 0) { + throw new Error('This matcher does not take any expected argument.'); + } + return { message: () => `result: ${actual}`, pass: actual === 'result', }; }; -type CustomContextAndExpected = ( - this: CustomContext, +type CustomStateAndExpected = ( + this: CustomState, actual: unknown, count: number, -) => ExpectationResult; +) => any; -const customContextAndExpected: MatcherFunctionWithContext< - CustomContext, +const customStateAndExpected: MatcherFunctionWithState< + CustomState, [count: number] > = function (actual: unknown, count: unknown) { - expectType(this); + expectType(this); expectType(this.customMethod()); return { @@ -195,4 +210,4 @@ const customContextAndExpected: MatcherFunctionWithContext< }; }; -expectType(customContextAndExpected); +expectAssignable(customStateAndExpected); diff --git a/packages/expect/src/index.ts b/packages/expect/src/index.ts index ac6a2d9db36b..855339906134 100644 --- a/packages/expect/src/index.ts +++ b/packages/expect/src/index.ts @@ -52,9 +52,8 @@ import type { export type { AsymmetricMatchers, Expect, - ExpectationResult, MatcherFunction, - MatcherFunctionWithContext, + MatcherFunctionWithState, MatcherState, Matchers, } from './types'; diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index 232c970ab759..94a03e70f127 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -20,25 +20,25 @@ export type AsyncExpectationResult = Promise; export type ExpectationResult = SyncExpectationResult | AsyncExpectationResult; -export type MatcherFunctionWithContext< - Context extends MatcherState = MatcherState, - Expected extends Array = [], -> = ( - this: Context, - actual: unknown, - ...expected: Expected -) => ExpectationResult; +export type MatcherFunctionWithState< + State extends MatcherState = MatcherState, + Expected extends Array = [] /** TODO should be: extends Array = [] */, +> = (this: State, actual: unknown, ...expected: Expected) => ExpectationResult; -export type MatcherFunction = []> = - MatcherFunctionWithContext; +export type MatcherFunction = []> = + MatcherFunctionWithState; // TODO should be replaced with `MatcherFunctionWithContext` -export type RawMatcherFn = { - (this: T, actual: any, ...expected: Array): ExpectationResult; +export type RawMatcherFn = { + (this: State, actual: any, ...expected: Array): ExpectationResult; /** @internal */ [INTERNAL_MATCHER_FLAG]?: boolean; }; +export type MatchersObject = { + [name: string]: RawMatcherFn; +}; + export type ThrowingMatcherFn = (actual: any) => void; export type PromiseMatcherFn = (actual: any) => Promise; @@ -69,9 +69,7 @@ export interface AsymmetricMatcher { getExpectedType?(): string; toAsymmetricMatcher?(): string; } -export type MatchersObject = { - [name: string]: RawMatcherFn; -}; + export type ExpectedAssertionsErrors = Array<{ actual: string | number; error: Error; @@ -202,6 +200,10 @@ export interface Matchers, T = unknown> { * For comparing floating point numbers. */ toBeLessThanOrEqual(expected: number | bigint): R; + /** + * Used to check that a variable is NaN. + */ + toBeNaN(): R; /** * This is the same as `.toBe(null)` but the error messages are a bit nicer. * So use `.toBeNull()` when you want to check that something is null. @@ -217,10 +219,6 @@ export interface Matchers, T = unknown> { * Used to check that a variable is undefined. */ toBeUndefined(): R; - /** - * Used to check that a variable is NaN. - */ - toBeNaN(): R; /** * Used when you want to check that an item is in a list. * For testing the items in the list, this uses `===`, a strict equality check. diff --git a/packages/jest-jasmine2/src/types.ts b/packages/jest-jasmine2/src/types.ts index 750a43f9b13a..c444f8e77a98 100644 --- a/packages/jest-jasmine2/src/types.ts +++ b/packages/jest-jasmine2/src/types.ts @@ -7,7 +7,7 @@ import type {AssertionError} from 'assert'; import type {Config} from '@jest/types'; -import type {Expect, ExpectationResult} from 'expect'; +import type {Expect} from 'expect'; import type CallTracker from './jasmine/CallTracker'; import type Env from './jasmine/Env'; import type JsApiReporter from './jasmine/JsApiReporter'; @@ -48,8 +48,8 @@ export interface Spy extends Record { type JasmineMatcher = { (matchersUtil: unknown, context: unknown): JasmineMatcher; - compare(...args: Array): ExpectationResult; - negativeCompare(...args: Array): ExpectationResult; + compare(...args: Array): unknown; + negativeCompare(...args: Array): unknown; }; export type JasmineMatchersObject = {[id: string]: JasmineMatcher}; diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index ad158dd6d1e2..5af2cbdf69e7 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -7,7 +7,7 @@ import * as fs from 'graceful-fs'; import type {Config} from '@jest/types'; -import type {ExpectationResult} from 'expect'; +import type {MatcherFunctionWithState} from 'expect'; import type {FS as HasteFS} from 'jest-haste-map'; import { BOLD_WEIGHT, @@ -156,12 +156,11 @@ export const cleanup = ( }; }; -export const toMatchSnapshot = function ( - this: Context, +export const toMatchSnapshot: MatcherFunctionWithState = function ( received: unknown, propertiesOrHint?: object | Config.Path, hint?: Config.Path, -): ExpectationResult { +) { const matcherName = 'toMatchSnapshot'; let properties; @@ -215,70 +214,70 @@ export const toMatchSnapshot = function ( }); }; -export const toMatchInlineSnapshot = function ( - this: Context, - received: unknown, - propertiesOrSnapshot?: object | string, - inlineSnapshot?: string, -): ExpectationResult { - const matcherName = 'toMatchInlineSnapshot'; - let properties; +export const toMatchInlineSnapshot: MatcherFunctionWithState = + function ( + received: unknown, + propertiesOrSnapshot?: object | string, + inlineSnapshot?: string, + ) { + const matcherName = 'toMatchInlineSnapshot'; + let properties; + + const length = arguments.length; + if (length === 2 && typeof propertiesOrSnapshot === 'string') { + inlineSnapshot = propertiesOrSnapshot; + } else if (length >= 2) { + const options: MatcherHintOptions = { + isNot: this.isNot, + promise: this.promise, + }; + if (length === 3) { + options.secondArgument = SNAPSHOT_ARG; + options.secondArgumentColor = noColor; + } - const length = arguments.length; - if (length === 2 && typeof propertiesOrSnapshot === 'string') { - inlineSnapshot = propertiesOrSnapshot; - } else if (length >= 2) { - const options: MatcherHintOptions = { - isNot: this.isNot, - promise: this.promise, - }; - if (length === 3) { - options.secondArgument = SNAPSHOT_ARG; - options.secondArgumentColor = noColor; - } + if ( + typeof propertiesOrSnapshot !== 'object' || + propertiesOrSnapshot === null + ) { + throw new Error( + matcherErrorMessage( + matcherHint(matcherName, undefined, PROPERTIES_ARG, options), + `Expected ${EXPECTED_COLOR('properties')} must be an object`, + printWithType( + 'Expected properties', + propertiesOrSnapshot, + printExpected, + ), + ), + ); + } - if ( - typeof propertiesOrSnapshot !== 'object' || - propertiesOrSnapshot === null - ) { - throw new Error( - matcherErrorMessage( - matcherHint(matcherName, undefined, PROPERTIES_ARG, options), - `Expected ${EXPECTED_COLOR('properties')} must be an object`, - printWithType( - 'Expected properties', - propertiesOrSnapshot, - printExpected, + if (length === 3 && typeof inlineSnapshot !== 'string') { + throw new Error( + matcherErrorMessage( + matcherHint(matcherName, undefined, PROPERTIES_ARG, options), + 'Inline snapshot must be a string', + printWithType('Inline snapshot', inlineSnapshot, serialize), ), - ), - ); - } + ); + } - if (length === 3 && typeof inlineSnapshot !== 'string') { - throw new Error( - matcherErrorMessage( - matcherHint(matcherName, undefined, PROPERTIES_ARG, options), - 'Inline snapshot must be a string', - printWithType('Inline snapshot', inlineSnapshot, serialize), - ), - ); + properties = propertiesOrSnapshot; } - properties = propertiesOrSnapshot; - } - - return _toMatchSnapshot({ - context: this, - inlineSnapshot: - inlineSnapshot !== undefined - ? stripAddedIndentation(inlineSnapshot) - : undefined, - isInline: true, - matcherName, - properties, - received, - }); -}; + return _toMatchSnapshot({ + context: this, + inlineSnapshot: + inlineSnapshot !== undefined + ? stripAddedIndentation(inlineSnapshot) + : undefined, + isInline: true, + matcherName, + properties, + received, + }); + }; const _toMatchSnapshot = (config: MatchSnapshotConfig) => { const {context, hint, inlineSnapshot, isInline, matcherName, properties} = @@ -409,67 +408,59 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => { }; }; -export const toThrowErrorMatchingSnapshot = function ( - this: Context, - received: unknown, - hint?: string, - fromPromise?: boolean, -): ExpectationResult { - const matcherName = 'toThrowErrorMatchingSnapshot'; +export const toThrowErrorMatchingSnapshot: MatcherFunctionWithState = + function (received: unknown, hint?: string, fromPromise?: boolean) { + const matcherName = 'toThrowErrorMatchingSnapshot'; - // Future breaking change: Snapshot hint must be a string - // if (hint !== undefined && typeof hint !== string) {} + // Future breaking change: Snapshot hint must be a string + // if (hint !== undefined && typeof hint !== string) {} + + return _toThrowErrorMatchingSnapshot( + { + context: this, + hint, + isInline: false, + matcherName, + received, + }, + fromPromise, + ); + }; - return _toThrowErrorMatchingSnapshot( - { - context: this, - hint, - isInline: false, - matcherName, - received, - }, - fromPromise, - ); -}; +export const toThrowErrorMatchingInlineSnapshot: MatcherFunctionWithState = + function (received: unknown, inlineSnapshot?: string, fromPromise?: boolean) { + const matcherName = 'toThrowErrorMatchingInlineSnapshot'; -export const toThrowErrorMatchingInlineSnapshot = function ( - this: Context, - received: unknown, - inlineSnapshot?: string, - fromPromise?: boolean, -): ExpectationResult { - const matcherName = 'toThrowErrorMatchingInlineSnapshot'; + if (inlineSnapshot !== undefined && typeof inlineSnapshot !== 'string') { + const options: MatcherHintOptions = { + expectedColor: noColor, + isNot: this.isNot, + promise: this.promise, + }; - if (inlineSnapshot !== undefined && typeof inlineSnapshot !== 'string') { - const options: MatcherHintOptions = { - expectedColor: noColor, - isNot: this.isNot, - promise: this.promise, - }; + throw new Error( + matcherErrorMessage( + matcherHint(matcherName, undefined, SNAPSHOT_ARG, options), + 'Inline snapshot must be a string', + printWithType('Inline snapshot', inlineSnapshot, serialize), + ), + ); + } - throw new Error( - matcherErrorMessage( - matcherHint(matcherName, undefined, SNAPSHOT_ARG, options), - 'Inline snapshot must be a string', - printWithType('Inline snapshot', inlineSnapshot, serialize), - ), + return _toThrowErrorMatchingSnapshot( + { + context: this, + inlineSnapshot: + inlineSnapshot !== undefined + ? stripAddedIndentation(inlineSnapshot) + : undefined, + isInline: true, + matcherName, + received, + }, + fromPromise, ); - } - - return _toThrowErrorMatchingSnapshot( - { - context: this, - inlineSnapshot: - inlineSnapshot !== undefined - ? stripAddedIndentation(inlineSnapshot) - : undefined, - isInline: true, - matcherName, - received, - }, - fromPromise, - ); -}; + }; const _toThrowErrorMatchingSnapshot = ( config: MatchSnapshotConfig, From f0bca5d0db8459c5254ff4bd345f72374a999b9a Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Tue, 15 Feb 2022 21:29:23 +0200 Subject: [PATCH 11/12] tweak changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8caf3e93eb1b..f198e9e274cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- `[expect]` Expose `AsymmetricMatchers`, `MatcherFunction` and `MatcherFunctionWithState` interfaces ([#12363](https://github.com/facebook/jest/pull/12363), [#12376](https://github.com/facebook/jest/pull/12376)) - `[jest-config]` [**BREAKING**] Stop shipping `jest-environment-jsdom` by default ([#12354](https://github.com/facebook/jest/pull/12354)) - `[jest-config]` [**BREAKING**] Stop shipping `jest-jasmine2` by default ([#12355](https://github.com/facebook/jest/pull/12355)) - `[jest-environment-jsdom]` [**BREAKING**] Upgrade jsdom to 19.0.0 ([#12290](https://github.com/facebook/jest/pull/12290)) @@ -14,7 +15,6 @@ ### Fixes - `[expect]` Move typings of `.not`, `.rejects` and `.resolves` modifiers outside of `Matchers` interface ([#12346](https://github.com/facebook/jest/pull/12346)) -- `[expect]` Expose `AsymmetricMatchers` and `ExpectationResult` interfaces ([#12363](https://github.com/facebook/jest/pull/12363), [#12376](https://github.com/facebook/jest/pull/12376)) - `[jest-environment-jsdom]` Make `jsdom` accessible to extending environments again ([#12232](https://github.com/facebook/jest/pull/12232)) - `[jest-jasmine2, jest-types]` [**BREAKING**] Move all `jasmine` specific types from `@jest/types` to its own package ([#12125](https://github.com/facebook/jest/pull/12125)) From 458955310791654720c3376e19bc3a5ef09c40ca Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Tue, 15 Feb 2022 21:36:31 +0200 Subject: [PATCH 12/12] remove demo --- packages/expect/__typetests__/expect.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/expect/__typetests__/expect.test.ts b/packages/expect/__typetests__/expect.test.ts index 4ea6b99d31e9..6f5d73bcced0 100644 --- a/packages/expect/__typetests__/expect.test.ts +++ b/packages/expect/__typetests__/expect.test.ts @@ -136,7 +136,7 @@ expectAssignable(toBeWithinRange); type AllowOmittingExpected = (this: MatcherState, actual: unknown) => any; -const allowOmittingExpected: MatcherFunction<[]> = ( +const allowOmittingExpected: MatcherFunction = ( actual: unknown, ...expect: Array ) => {