diff --git a/packages/jest/src/__tests__/extensions.spec.js b/packages/jest/src/__tests__/extensions.spec.js index 63f03498..ac834dd3 100644 --- a/packages/jest/src/__tests__/extensions.spec.js +++ b/packages/jest/src/__tests__/extensions.spec.js @@ -3,16 +3,24 @@ import { describe, it, beforeAll, afterAll, expect } from '@jest/globals'; import fetchMockModule from '../index'; const fetchMock = fetchMockModule.default; -describe('expect extensions', () => { - [ - 'Fetched', - 'Got:get', - 'Posted:post', - 'Put:put', - 'Deleted:delete', - 'FetchedHead:head', - 'Patched:patch', - ].forEach((verbs) => { +const humanVerbToMethods = [ + 'Fetched', + 'Got:get', + 'Posted:post', + 'Put:put', + 'Deleted:delete', + 'FetchedHead:head', + 'Patched:patch', +]; + +// initialize a mock here so fetch is patched across all tests +fetchMock.mockGlobal(); + +describe.each([ + ['patched fetch input', fetch], + ['fetchMock input', fetchMock], +])('expect extensions %s', (_str, expectInput) => { + humanVerbToMethods.forEach((verbs) => { const [humanVerb, method] = verbs.split(':'); describe(`${humanVerb} expectations`, () => { describe('when no calls', () => { @@ -21,24 +29,26 @@ describe('expect extensions', () => { }); afterAll(() => fetchMock.mockReset()); it(`toHave${humanVerb} should be falsy`, () => { - expect(fetch).not[`toHave${humanVerb}`]('http://example.com/path'); + expect(expectInput).not[`toHave${humanVerb}`]( + 'http://example.com/path', + ); }); it(`toHaveLast${humanVerb} should be falsy`, () => { - expect(fetch).not[`toHaveLast${humanVerb}`]( + expect(expectInput).not[`toHaveLast${humanVerb}`]( 'http://example.com/path', ); }); it(`toHaveNth${humanVerb} should be falsy`, () => { - expect(fetch).not[`toHaveNth${humanVerb}`]( + expect(expectInput).not[`toHaveNth${humanVerb}`]( 1, 'http://example.com/path', ); }); it(`toHave${humanVerb}Times should be falsy`, () => { - expect(fetch).not[`toHave${humanVerb}Times`]( + expect(expectInput).not[`toHave${humanVerb}Times`]( 1, 'http://example.com/path', ); @@ -63,15 +73,17 @@ describe('expect extensions', () => { afterAll(() => fetchMock.mockReset()); it('matches with just url', () => { - expect(fetch)[`toHave${humanVerb}`]('http://example.com/path'); + expect(expectInput)[`toHave${humanVerb}`]('http://example.com/path'); }); it('matches with fetch-mock matcher', () => { - expect(fetch)[`toHave${humanVerb}`]('begin:http://example.com/path'); + expect(expectInput)[`toHave${humanVerb}`]( + 'begin:http://example.com/path', + ); }); it('matches with matcher and options', () => { - expect(fetch)[`toHave${humanVerb}`]('http://example.com/path', { + expect(expectInput)[`toHave${humanVerb}`]('http://example.com/path', { headers: { test: 'header', }, @@ -79,15 +91,18 @@ describe('expect extensions', () => { }); it("doesn't match if matcher but not options is correct", () => { - expect(fetch).not[`toHave${humanVerb}`]('http://example.com/path', { - headers: { - test: 'not-header', + expect(expectInput).not[`toHave${humanVerb}`]( + 'http://example.com/path', + { + headers: { + test: 'not-header', + }, }, - }); + ); }); it("doesn't match if options but not matcher is correct", () => { - expect(fetch).not[`toHave${humanVerb}`]( + expect(expectInput).not[`toHave${humanVerb}`]( 'http://example-no.com/path', { headers: { @@ -110,25 +125,30 @@ describe('expect extensions', () => { afterAll(() => fetchMock.mockReset()); it('matches with just url', () => { - expect(fetch)[`toHaveLast${humanVerb}`]('http://example.com/path'); + expect(expectInput)[`toHaveLast${humanVerb}`]( + 'http://example.com/path', + ); }); it('matches with fetch-mock matcher', () => { - expect(fetch)[`toHaveLast${humanVerb}`]( + expect(expectInput)[`toHaveLast${humanVerb}`]( 'begin:http://example.com/path', ); }); it('matches with matcher and options', () => { - expect(fetch)[`toHaveLast${humanVerb}`]('http://example.com/path', { - headers: { - test: 'header', + expect(expectInput)[`toHaveLast${humanVerb}`]( + 'http://example.com/path', + { + headers: { + test: 'header', + }, }, - }); + ); }); it("doesn't match if matcher but not options is correct", () => { - expect(fetch).not[`toHaveLast${humanVerb}`]( + expect(expectInput).not[`toHaveLast${humanVerb}`]( 'http://example.com/path', { headers: { @@ -139,7 +159,7 @@ describe('expect extensions', () => { }); it("doesn't match if options but not matcher is correct", () => { - expect(fetch).not[`toHaveLast${humanVerb}`]( + expect(expectInput).not[`toHaveLast${humanVerb}`]( 'http://example-no.com/path', { headers: { @@ -169,18 +189,21 @@ describe('expect extensions', () => { afterAll(() => fetchMock.mockReset()); it('matches with just url', () => { - expect(fetch)[`toHaveNth${humanVerb}`](2, 'http://example2.com/path'); + expect(expectInput)[`toHaveNth${humanVerb}`]( + 2, + 'http://example2.com/path', + ); }); it('matches with fetch-mock matcher', () => { - expect(fetch)[`toHaveNth${humanVerb}`]( + expect(expectInput)[`toHaveNth${humanVerb}`]( 2, 'begin:http://example2.com/path', ); }); it('matches with matcher and options', () => { - expect(fetch)[`toHaveNth${humanVerb}`]( + expect(expectInput)[`toHaveNth${humanVerb}`]( 2, 'http://example2.com/path', { @@ -192,7 +215,7 @@ describe('expect extensions', () => { }); it("doesn't match if matcher but not options is correct", () => { - expect(fetch).not[`toHaveNth${humanVerb}`]( + expect(expectInput).not[`toHaveNth${humanVerb}`]( 2, 'http://example2.com/path', { @@ -204,7 +227,7 @@ describe('expect extensions', () => { }); it("doesn't match if options but not matcher is correct", () => { - expect(fetch).not[`toHaveNth${humanVerb}`]( + expect(expectInput).not[`toHaveNth${humanVerb}`]( 2, 'http://example-no.com/path', { @@ -216,7 +239,7 @@ describe('expect extensions', () => { }); it("doesn't match if wrong n", () => { - expect(fetch).not[`toHaveNth${humanVerb}`]( + expect(expectInput).not[`toHaveNth${humanVerb}`]( 1, 'http://example2.com/path', ); @@ -242,21 +265,21 @@ describe('expect extensions', () => { afterAll(() => fetchMock.mockReset()); it('matches with just url', () => { - expect(fetch)[`toHave${humanVerb}Times`]( + expect(expectInput)[`toHave${humanVerb}Times`]( 2, 'http://example.com/path', ); }); it('matches with fetch-mock matcher', () => { - expect(fetch)[`toHave${humanVerb}Times`]( + expect(expectInput)[`toHave${humanVerb}Times`]( 2, 'begin:http://example.com/path', ); }); it('matches with matcher and options', () => { - expect(fetch)[`toHave${humanVerb}Times`]( + expect(expectInput)[`toHave${humanVerb}Times`]( 2, 'http://example.com/path', { @@ -268,7 +291,7 @@ describe('expect extensions', () => { }); it("doesn't match if matcher but not options is correct", () => { - expect(fetch).not[`toHave${humanVerb}Times`]( + expect(expectInput).not[`toHave${humanVerb}Times`]( 2, 'http://example.com/path', { @@ -280,7 +303,7 @@ describe('expect extensions', () => { }); it("doesn't match if options but not matcher is correct", () => { - expect(fetch).not[`toHave${humanVerb}Times`]( + expect(expectInput).not[`toHave${humanVerb}Times`]( 2, 'http://example-no.com/path', { @@ -292,14 +315,14 @@ describe('expect extensions', () => { }); it("doesn't match if too few calls", () => { - expect(fetch).not[`toHave${humanVerb}Times`]( + expect(expectInput).not[`toHave${humanVerb}Times`]( 1, 'http://example.com/path', ); }); it("doesn't match if too many calls", () => { - expect(fetch).not[`toHave${humanVerb}Times`]( + expect(expectInput).not[`toHave${humanVerb}Times`]( 3, 'http://example.com/path', ); @@ -326,15 +349,29 @@ describe('expect extensions', () => { }); afterAll(() => fetchMock.mockReset()); // it('toBeDone should be falsy only if routes defined', () => { - // expect(fetch).not.toBeDone(); - // expect(fetch).not.toBeDone('my-route'); + // expect(expectInput).not.toBeDone(); + // expect(expectInput).not.toBeDone('my-route'); // }); it('matches with just url', () => { - expect(fetch).toBeDone('route1'); + expect(expectInput).toBeDone('route1'); }); it("doesn't match if too few calls", () => { - expect(fetch).not.toBeDone('route2'); + expect(expectInput).not.toBeDone('route2'); + }); + }); +}); + +describe('expect extensions: bad inputs', () => { + humanVerbToMethods.forEach((verbs) => { + const [humanVerb] = verbs.split(':'); + 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 + expect({})[`toHave${humanVerb}`]('http://example.com/path'); + }).toThrow( + 'Unable to get fetchMock instance! Please make sure you passed a patched fetch or fetchMock!', + ); }); }); }); diff --git a/packages/jest/src/index.ts b/packages/jest/src/index.ts index 7c78ca2d..0cfc3508 100644 --- a/packages/jest/src/index.ts +++ b/packages/jest/src/index.ts @@ -67,6 +67,7 @@ declare global { // Type-narrow expect for FetchMock interface Expect { (actual: FetchMock): FetchMockMatchers; + (actual: typeof fetch): FetchMockMatchers; } } } diff --git a/packages/jest/src/jest-extensions.ts b/packages/jest/src/jest-extensions.ts index 5e817583..74a1c7fe 100644 --- a/packages/jest/src/jest-extensions.ts +++ b/packages/jest/src/jest-extensions.ts @@ -1,6 +1,6 @@ import { expect } from '@jest/globals'; import type { SyncExpectationResult } from 'expect'; -import type { +import { FetchMock, RouteName, CallHistoryFilter, @@ -9,18 +9,32 @@ import type { import { HumanVerbMethodNames, HumanVerbs, + PatchedFetch, RawFetchMockMatchers, } from './types.js'; +function getFetchMockFromInput(input: PatchedFetch | FetchMock) { + const fetchMock = (input as PatchedFetch)['fetchMock'] + ? (input as PatchedFetch).fetchMock + : input; + if (!fetchMock || !(fetchMock instanceof FetchMock)) { + throw new Error( + 'Unable to get fetchMock instance! Please make sure you passed a patched fetch or fetchMock!', + ); + } + return fetchMock; +} + const methodlessExtensions: Pick< RawFetchMockMatchers, HumanVerbMethodNames<'Fetched'> > = { toHaveFetched: ( - { fetchMock }: { fetchMock: FetchMock }, + input: PatchedFetch | FetchMock, filter: CallHistoryFilter, options: UserRouteConfig, ): SyncExpectationResult => { + const fetchMock = getFetchMockFromInput(input); if (fetchMock.callHistory.called(filter, options)) { return { pass: true, message: () => 'fetch was called as expected' }; } @@ -31,10 +45,11 @@ const methodlessExtensions: Pick< }; }, toHaveLastFetched: ( - { fetchMock }: { fetchMock: FetchMock }, + input: PatchedFetch | FetchMock, filter: CallHistoryFilter, options: UserRouteConfig, ): SyncExpectationResult => { + const fetchMock = getFetchMockFromInput(input); const allCalls = fetchMock.callHistory.calls(); if (!allCalls.length) { return { @@ -57,11 +72,12 @@ const methodlessExtensions: Pick< }, toHaveNthFetched: ( - { fetchMock }: { fetchMock: FetchMock }, + input: PatchedFetch | FetchMock, n: number, filter: CallHistoryFilter, options: UserRouteConfig, ): SyncExpectationResult => { + const fetchMock = getFetchMockFromInput(input); const nthCall = fetchMock.callHistory.calls()[n - 1]; const matchingCalls = fetchMock.callHistory.calls(filter, options); if (matchingCalls.some((call) => call === nthCall)) { @@ -78,11 +94,12 @@ const methodlessExtensions: Pick< }, toHaveFetchedTimes: ( - { fetchMock }: { fetchMock: FetchMock }, + input: PatchedFetch | FetchMock, times: number, filter: CallHistoryFilter, options: UserRouteConfig, ): SyncExpectationResult => { + const fetchMock = getFetchMockFromInput(input); const calls = fetchMock.callHistory.calls(filter, options); if (calls.length === times) { return { @@ -102,9 +119,10 @@ expect.extend(methodlessExtensions); expect.extend({ toBeDone: ( - { fetchMock }: { fetchMock: FetchMock }, + input: PatchedFetch | FetchMock, routes: RouteName | RouteName[], ): SyncExpectationResult => { + const fetchMock = getFetchMockFromInput(input); const done = fetchMock.callHistory.done(routes); if (done) { return { pass: true, message: () => '' }; diff --git a/packages/jest/src/types.ts b/packages/jest/src/types.ts index 74480178..18509a05 100644 --- a/packages/jest/src/types.ts +++ b/packages/jest/src/types.ts @@ -71,12 +71,19 @@ export type FetchMockMatchers = { }; // types for use doing some intermediate type checking in extensions to make sure things don't get out of sync +/** + * This reflects the Object.assign that FetchMock does on the fetch function + */ +export type PatchedFetch = { + fetchMock: FetchMock; +}; + /** * 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 }, + input: PatchedFetch | FetchMock, ...args: Parameters ) => ReturnType;