From 033048a47ffc07508fc0cb2ce79078b4facb86fb Mon Sep 17 00:00:00 2001 From: hanseltime Date: Tue, 26 Nov 2024 13:49:50 -0700 Subject: [PATCH] fix: adding types to jest matchers --- packages/jest/src/index.ts | 16 +++++ packages/jest/src/jest-extensions.ts | 38 +++++++----- packages/jest/src/types.ts | 91 ++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 packages/jest/src/types.ts diff --git a/packages/jest/src/index.ts b/packages/jest/src/index.ts index 81b1e257..7c78ca2d 100644 --- a/packages/jest/src/index.ts +++ b/packages/jest/src/index.ts @@ -5,6 +5,8 @@ import { } from 'fetch-mock'; import './jest-extensions.js'; import type { Jest } from '@jest/environment'; +import type { FetchMockMatchers } from './types.js'; +export { FetchMockMatchers } from './types.js'; type MockResetOptions = { includeSticky: boolean; @@ -55,3 +57,17 @@ const fetchMockJest = new FetchMockJest({ }); export default fetchMockJest; + +/* eslint-disable @typescript-eslint/no-namespace */ +/** + * Export types on the expect object + */ +declare global { + namespace jest { + // Type-narrow expect for FetchMock + interface Expect { + (actual: FetchMock): FetchMockMatchers; + } + } +} +/* eslint-enable @typescript-eslint/no-namespace */ diff --git a/packages/jest/src/jest-extensions.ts b/packages/jest/src/jest-extensions.ts index 09b927be..5e817583 100644 --- a/packages/jest/src/jest-extensions.ts +++ b/packages/jest/src/jest-extensions.ts @@ -6,7 +6,16 @@ import type { CallHistoryFilter, UserRouteConfig, } from 'fetch-mock'; -const methodlessExtensions = { +import { + HumanVerbMethodNames, + HumanVerbs, + RawFetchMockMatchers, +} from './types.js'; + +const methodlessExtensions: Pick< + RawFetchMockMatchers, + HumanVerbMethodNames<'Fetched'> +> = { toHaveFetched: ( { fetchMock }: { fetchMock: FetchMock }, filter: CallHistoryFilter, @@ -128,25 +137,24 @@ function scopeExpectationNameToMethod(name: string, humanVerb: string): string { return name.replace('Fetched', humanVerb); } -[ - 'Got:get', - 'Posted:post', - 'Put:put', - 'Deleted:delete', - 'FetchedHead:head', - 'Patched:patch', -].forEach((verbs) => { - const [humanVerb, method] = verbs.split(':'); +const expectMethodNameToMethodMap: { + [humanVerb in Exclude]: string; +} = { + Got: 'get', + Posted: 'post', + Put: 'put', + Deleted: 'delete', + FetchedHead: 'head', + Patched: 'patch', +}; - const extensions: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: (...args: any[]) => SyncExpectationResult; - } = Object.fromEntries( +Object.entries(expectMethodNameToMethodMap).forEach(([humanVerb, method]) => { + const extensions = Object.fromEntries( Object.entries(methodlessExtensions).map(([name, func]) => [ scopeExpectationNameToMethod(name, humanVerb), scopeExpectationFunctionToMethod(func, method), ]), - ); + ) as Omit>; expect.extend(extensions); }); diff --git a/packages/jest/src/types.ts b/packages/jest/src/types.ts new file mode 100644 index 00000000..74480178 --- /dev/null +++ b/packages/jest/src/types.ts @@ -0,0 +1,91 @@ +import type { CallHistoryFilter, FetchMock, UserRouteConfig } from 'fetch-mock'; +import type { SyncExpectationResult } from 'expect'; + +export type HumanVerbs = + | 'Got' + | 'Posted' + | 'Put' + | 'Deleted' + | 'FetchedHead' + | 'Patched' + | 'Fetched'; + +/** + * Verify that a particular call for the HTTP method implied in the function name + * has occurred + */ +export type ToHaveFunc = ( + filter: CallHistoryFilter, + options: UserRouteConfig, +) => SyncExpectationResult; + +/** + * Verify that a particular Nth call for the HTTP method implied in the function name + * has occurred + */ +export type ToHaveNthFunc = ( + n: number, + filter: CallHistoryFilter, + options: UserRouteConfig, +) => SyncExpectationResult; + +/** + * Verify that a particular call for the HTTP method implied in the function name + * has been made N times + */ +export type ToHaveTimesFunc = ( + times: number, + filter: CallHistoryFilter, + options: UserRouteConfig, +) => SyncExpectationResult; + +export type FetchMockMatchers = { + toHaveFetched: ToHaveFunc; + toHaveLastFetched: ToHaveFunc; + toHaveFetchedTimes: ToHaveTimesFunc; + toHaveNthFetched: ToHaveNthFunc; + toHaveGot: ToHaveFunc; + toHaveLastGot: ToHaveFunc; + toHaveGotTimes: ToHaveTimesFunc; + toHaveNthGot: ToHaveNthFunc; + toHavePosted: ToHaveFunc; + toHaveLastPosted: ToHaveFunc; + toHavePostedTimes: ToHaveTimesFunc; + toHaveNthPosted: ToHaveNthFunc; + toHavePut: ToHaveFunc; + toHaveLastPut: ToHaveFunc; + toHavePutTimes: ToHaveTimesFunc; + toHaveNthPut: ToHaveNthFunc; + toHaveDeleted: ToHaveFunc; + toHaveLastDeleted: ToHaveFunc; + toHaveDeletedTimes: ToHaveTimesFunc; + toHaveNthDeleted: ToHaveNthFunc; + toHaveFetchedHead: ToHaveFunc; + toHaveLastFetchedHead: ToHaveFunc; + toHaveFetchedHeadTimes: ToHaveTimesFunc; + toHaveNthFetchedHead: ToHaveNthFunc; + toHavePatched: ToHaveFunc; + toHaveLastPatched: ToHaveFunc; + toHavePatchedTimes: ToHaveTimesFunc; + toHaveNthPatched: ToHaveNthFunc; +}; + +// types for use doing some intermediate type checking in extensions to make sure things don't get out of sync +/** + * This type allows us to take the Matcher type and creat another one + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type RawMatcher any> = ( + input: { fetchMock: FetchMock }, + ...args: Parameters +) => ReturnType; + +export type RawFetchMockMatchers = { + [k in keyof FetchMockMatchers]: RawMatcher; +}; + +export type HumanVerbMethodNames = + | `toHave${M}` + | `toHaveLast${M}` + | `toHave${M}Times` + | `toHaveNth${M}`;