Skip to content

Commit

Permalink
fix: incorrect Jest extension TypeScript type
Browse files Browse the repository at this point in the history
  • Loading branch information
Yihao-G committed Dec 25, 2024
1 parent b63230f commit 7d4a55a
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { describe, it, beforeAll, afterAll, expect } from '@jest/globals';
import { afterAll, beforeAll, describe, expect, it } from '@jest/globals';

import fetchMockModule from '../index';
const fetchMock = fetchMockModule.default;
import fetchMock from '../index';

const humanVerbToMethods = [
'Fetched',
'Got:get',
'Posted:post',
'Put:put',
'Deleted:delete',
'FetchedHead:head',
'Patched:patch',
];
{ humanVerb: 'Fetched', method: 'get' },
{ humanVerb: 'Got', method: 'get' },
{ humanVerb: 'Posted', method: 'post' },
{ humanVerb: 'Put', method: 'put' },
{ humanVerb: 'Deleted', method: 'delete' },
{ humanVerb: 'FetchedHead', method: 'head' },
{ humanVerb: 'Patched', method: 'patch' },
] as const;

// initialize a mock here so fetch is patched across all tests
fetchMock.mockGlobal();
Expand All @@ -20,34 +19,37 @@ describe.each([
['patched fetch input', fetch],
['fetchMock input', fetchMock],
])('expect extensions %s', (_str, expectInput) => {
humanVerbToMethods.forEach((verbs) => {
const [humanVerb, method] = verbs.split(':');
humanVerbToMethods.forEach(({ humanVerb, method }) => {
describe(`${humanVerb} expectations`, () => {
describe('when no calls', () => {
beforeAll(() => {
fetchMock.mockGlobal().route('*', 200, 'my-route');
});
afterAll(() => fetchMock.mockReset());
it(`toHave${humanVerb} should be falsy`, () => {
expect(expectInput).not[`toHave${humanVerb}`]();
expect(expectInput).not[`toHave${humanVerb}`](
'http://example.com/path',
);
});

it(`toHaveLast${humanVerb} should be falsy`, () => {
expect(expectInput).not[`toHaveLast${humanVerb}`]();
expect(expectInput).not[`toHaveLast${humanVerb}`](
'http://example.com/path',
);
});

it(`toHaveNth${humanVerb} should be falsy`, () => {
expect(expectInput).not[`toHaveNth${humanVerb}`](1);
expect(expectInput).not[`toHaveNth${humanVerb}`](
1,
'http://example.com/path',
);
});

it(`toHave${humanVerb}Times should be falsy`, () => {
expect(expectInput).not[`toHave${humanVerb}Times`](1);
expect(expectInput).not[`toHave${humanVerb}Times`](
1,
'http://example.com/path',
Expand All @@ -58,20 +60,24 @@ describe.each([
beforeAll(() => {
fetchMock.mockGlobal().route('*', 200);
fetch('http://example.com/path2', {
method: method || 'get',
method,
headers: {
test: 'header',
},
});
fetch('http://example.com/path', {
method: method || 'get',
method,
headers: {
test: 'header',
},
});
});
afterAll(() => fetchMock.mockReset());

it('matches without any matcher supplied', () => {
expect(expectInput)[`toHave${humanVerb}`]();
});

it('matches with just url', () => {
expect(expectInput)[`toHave${humanVerb}`]('http://example.com/path');
});
Expand Down Expand Up @@ -116,14 +122,18 @@ describe.each([
beforeAll(() => {
fetchMock.mockGlobal().route('*', 200);
fetch('http://example.com/path', {
method: method || 'get',
method,
headers: {
test: 'header',
},
});
});
afterAll(() => fetchMock.mockReset());

it('matches without any matcher supplied', () => {
expect(expectInput)[`toHaveLast${humanVerb}`]();
});

it('matches with just url', () => {
expect(expectInput)[`toHaveLast${humanVerb}`](
'http://example.com/path',
Expand Down Expand Up @@ -174,20 +184,24 @@ describe.each([
beforeAll(() => {
fetchMock.mockGlobal().route('*', 200);
fetch('http://example1.com/path', {
method: method || 'get',
method,
headers: {
test: 'header',
},
});
fetch('http://example2.com/path', {
method: method || 'get',
method,
headers: {
test: 'header',
},
});
});
afterAll(() => fetchMock.mockReset());

it('matches without any matcher supplied', () => {
expect(expectInput)[`toHaveNth${humanVerb}`](2);
});

it('matches with just url', () => {
expect(expectInput)[`toHaveNth${humanVerb}`](
2,
Expand Down Expand Up @@ -250,20 +264,24 @@ describe.each([
beforeAll(() => {
fetchMock.mockGlobal().route('*', 200);
fetch('http://example.com/path', {
method: method || 'get',
method,
headers: {
test: 'header',
},
});
fetch('http://example.com/path', {
method: method || 'get',
method,
headers: {
test: 'header',
},
});
});
afterAll(() => fetchMock.mockReset());

it('matches without any matcher supplied', () => {
expect(expectInput)[`toHave${humanVerb}Times`](2);
});

it('matches with just url', () => {
expect(expectInput)[`toHave${humanVerb}Times`](
2,
Expand Down Expand Up @@ -363,8 +381,7 @@ describe.each([
});

describe('expect extensions: bad inputs', () => {
humanVerbToMethods.forEach((verbs) => {
const [humanVerb] = verbs.split(':');
humanVerbToMethods.forEach(({ humanVerb }) => {
it(`${humanVerb} - throws an error if we the input is not patched with fetchMock`, () => {
expect(() => {
// This simulates a "fetch" implementation that doesn't have fetchMock
Expand Down
20 changes: 0 additions & 20 deletions packages/jest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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 = {
Expand Down Expand Up @@ -57,22 +56,3 @@ 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 & {
not: FetchMockMatchers;
};
(actual: typeof fetch): FetchMockMatchers & {
not: FetchMockMatchers;
};
}
}
}
/* eslint-enable @typescript-eslint/no-namespace */
11 changes: 10 additions & 1 deletion packages/jest/src/jest-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
PatchedFetch,
RawFetchMockMatchers,
} from './types.js';
import type { FetchMockMatchers } from './types.js';

function getFetchMockFromInput(input: PatchedFetch | FetchMock) {
const fetchMock = (input as PatchedFetch)['fetchMock']
Expand Down Expand Up @@ -172,7 +173,15 @@ Object.entries(expectMethodNameToMethodMap).forEach(([humanVerb, method]) => {
scopeExpectationNameToMethod(name, humanVerb),
scopeExpectationFunctionToMethod(func, method),
]),
) as Omit<RawFetchMockMatchers, HumanVerbMethodNames<'Fetched'>>;
) as Omit<RawFetchMockMatchers, HumanVerbMethodNames<'Fetched'> | 'toBeDone'>;

expect.extend(extensions);
});

declare module 'expect' {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface AsymmetricMatchers extends FetchMockMatchers {}

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface Matchers<R> extends FetchMockMatchers<R> {}
}
100 changes: 57 additions & 43 deletions packages/jest/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { CallHistoryFilter, FetchMock, UserRouteConfig } from 'fetch-mock';
import type {
CallHistoryFilter,
FetchMock,
RouteName,
UserRouteConfig,
} from 'fetch-mock';
import type { SyncExpectationResult } from 'expect';

export type HumanVerbs =
Expand All @@ -14,60 +19,67 @@ export type HumanVerbs =
* Verify that a particular call for the HTTP method implied in the function name
* has occurred
*/
export type ToHaveFunc = (
filter: CallHistoryFilter,
options: UserRouteConfig,
) => SyncExpectationResult;
export type ToHaveFunc<R> = (
filter?: CallHistoryFilter,
options?: UserRouteConfig,
) => R;

/**
* Verify that a particular Nth call for the HTTP method implied in the function name
* has occurred
*/
export type ToHaveNthFunc = (
export type ToHaveNthFunc<R> = (
n: number,
filter: CallHistoryFilter,
options: UserRouteConfig,
) => SyncExpectationResult;
filter?: CallHistoryFilter,
options?: UserRouteConfig,
) => R;

/**
* Verify that a particular call for the HTTP method implied in the function name
* has been made N times
*/
export type ToHaveTimesFunc = (
export type ToHaveTimesFunc<R> = (
times: number,
filter: CallHistoryFilter,
options: UserRouteConfig,
) => SyncExpectationResult;
filter?: CallHistoryFilter,
options?: UserRouteConfig,
) => R;

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;
/**
* Verify that a particular route names(s) has been called
* for expected number of times
*/
export type ToBeDoneFunc<R> = (routes?: RouteName | RouteName[]) => R;

export type FetchMockMatchers<R = void> = {
toHaveFetched: ToHaveFunc<R>;
toHaveLastFetched: ToHaveFunc<R>;
toHaveFetchedTimes: ToHaveTimesFunc<R>;
toHaveNthFetched: ToHaveNthFunc<R>;
toHaveGot: ToHaveFunc<R>;
toHaveLastGot: ToHaveFunc<R>;
toHaveGotTimes: ToHaveTimesFunc<R>;
toHaveNthGot: ToHaveNthFunc<R>;
toHavePosted: ToHaveFunc<R>;
toHaveLastPosted: ToHaveFunc<R>;
toHavePostedTimes: ToHaveTimesFunc<R>;
toHaveNthPosted: ToHaveNthFunc<R>;
toHavePut: ToHaveFunc<R>;
toHaveLastPut: ToHaveFunc<R>;
toHavePutTimes: ToHaveTimesFunc<R>;
toHaveNthPut: ToHaveNthFunc<R>;
toHaveDeleted: ToHaveFunc<R>;
toHaveLastDeleted: ToHaveFunc<R>;
toHaveDeletedTimes: ToHaveTimesFunc<R>;
toHaveNthDeleted: ToHaveNthFunc<R>;
toHaveFetchedHead: ToHaveFunc<R>;
toHaveLastFetchedHead: ToHaveFunc<R>;
toHaveFetchedHeadTimes: ToHaveTimesFunc<R>;
toHaveNthFetchedHead: ToHaveNthFunc<R>;
toHavePatched: ToHaveFunc<R>;
toHaveLastPatched: ToHaveFunc<R>;
toHavePatchedTimes: ToHaveTimesFunc<R>;
toHaveNthPatched: ToHaveNthFunc<R>;
toBeDone: ToBeDoneFunc<R>;
};

// types for use doing some intermediate type checking in extensions to make sure things don't get out of sync
Expand All @@ -88,7 +100,9 @@ type RawMatcher<T extends (...args: any[]) => any> = (
) => ReturnType<T>;

export type RawFetchMockMatchers = {
[k in keyof FetchMockMatchers]: RawMatcher<FetchMockMatchers[k]>;
[k in keyof FetchMockMatchers<SyncExpectationResult>]: RawMatcher<
FetchMockMatchers<SyncExpectationResult>[k]
>;
};

export type HumanVerbMethodNames<M extends HumanVerbs> =
Expand Down

0 comments on commit 7d4a55a

Please sign in to comment.