diff --git a/docs/docs/@fetch-mock/core/mocking-and-spying.md b/docs/docs/@fetch-mock/core/mocking-and-spying.md index 42e8ed25..ee9ac010 100644 --- a/docs/docs/@fetch-mock/core/mocking-and-spying.md +++ b/docs/docs/@fetch-mock/core/mocking-and-spying.md @@ -12,18 +12,37 @@ Wrapper around @fetch-mock/core that implements mocking of global fetch, includi In addition to the @fetch-mock/core API its methods are: -## mockGlobal() +## When using global fetch in your application + +### mockGlobal() Replaces `fetch` with `fm.fetchHandler` -## restoreGlobal() +### spyGlobal() + +Replaces `fetch` with `fm.fetchHandler`, but falls back to the network for any unmatched calls + +### spyRoute(matcher, name) + +Falls back to `fetch` for a specific route (which can be named). + +This can also be used when using non-global `fetch` (see `setFetchImplementation()` below). + +### restoreGlobal() Restores `fetch` to its original state -## spyGlobal() -Replaces `fetch` with `fm.fetchHandler`, but falls back to the network for any unmatched calls +## When using non-global fetch + +e.g. `const fetch = require('node-fetch')` + +Note that none of these methods actually replace your local implementation of `fetch` with `fetchMock.fetchHandler` - that is left to you to implement with the mocking library/approach of your choice. ## spyLocal(fetchImplementation) -When using a non-global implementation of `fetch` (e.g. `const fetch = require('node-fetch')`), this adds that implementation as the network fallback used by `fetchHandler`. Note that this _does not_ actually replace the implementation with `fetchHandler` - that is left to you to implement with the mocking library/approach of your choice. +Fall back to the provided `fetch` implementation for any calls unmatched by a route. + +## setfetchImplementation(fetchImplementation) + +When you wish to use `.spyRoute()` use this function first to provide a `fetch` implementation to use. diff --git a/docs/docs/@fetch-mock/core/resetting.md b/docs/docs/@fetch-mock/core/resetting.md index 98072f32..43ae4b01 100644 --- a/docs/docs/@fetch-mock/core/resetting.md +++ b/docs/docs/@fetch-mock/core/resetting.md @@ -26,6 +26,10 @@ A boolean indicating whether or not to remove the fallback route (added using `. Clears all data recorded for `fetch`'s calls. +## restoreGlobal() + +Restores global `fetch` to its original state if `.mockGlobal()` or `.spyGlobal()` have been used . + ## .createInstance() Can be used to create a standalone instance of fetch mock that is completely independent of other instances. diff --git a/packages/core/src/FetchMock.js b/packages/core/src/FetchMock.js index 5547d34b..158a5167 100644 --- a/packages/core/src/FetchMock.js +++ b/packages/core/src/FetchMock.js @@ -249,6 +249,21 @@ class FetchMockStandalone extends FetchMock { this.catch(({ args }) => this.#originalFetch(...args)); return this; } + + /** + * @param {RouteMatcher | UserRouteConfig} matcher + * @param {RouteName} [name] + * @this {FetchMockStandalone} + */ + spyRoute(matcher, name) { + if (!this.#originalFetch) { + throw new Error('fetch-mock: Cannot spy on a route without first calling .mockGlobal() or .setFetchImplementation() to reference a `fetch` implementation to fall through to') + } + // @ts-ignore + this.route(matcher, ({args}) => this.#originalFetch(...args), name); + return this; + } + /** * @param {typeof fetch} fetchImplementation * @this {FetchMockStandalone} @@ -260,6 +275,15 @@ class FetchMockStandalone extends FetchMock { return this; } + /** + * @param {typeof fetch} fetchImplementation + * @this {FetchMockStandalone} + */ + setFetchImplementation(fetchImplementation) { + this.#originalFetch = fetchImplementation; + return this; + } + createInstance() { return new FetchMockStandalone({ ...this.config }, this.router); } diff --git a/packages/core/src/__tests__/FetchMock/mock-and-spy.test.js b/packages/core/src/__tests__/FetchMock/mock-and-spy.test.js index 636190ee..3cc354f4 100644 --- a/packages/core/src/__tests__/FetchMock/mock-and-spy.test.js +++ b/packages/core/src/__tests__/FetchMock/mock-and-spy.test.js @@ -2,92 +2,6 @@ import { beforeEach, describe, expect, it } from 'vitest'; const { fetchMock } = testGlobals; -describe('fallbackToNetwork', () => { - let fm; - beforeEach(() => { - fm = fetchMock.createInstance(); - }); - it('error by default', () => { - expect(() => fm.fetchHandler('http://unmocked.com')).toThrow(); - }); - - it('not error when configured globally', () => { - globalThis.fetch = async () => ({ status: 202 }); //eslint-disable-line require-await - fm.config.fallbackToNetwork = true; - fm.mock('http://mocked.com', 201); - expect(() => fm.fetchHandler('http://unmocked.com')).not.toThrow(); - delete globalThis.fetch; - }); - - it('actually falls back to network when configured globally', async () => { - globalThis.fetch = async () => ({ status: 202 }); //eslint-disable-line require-await - fetchMock.config.fallbackToNetwork = true; - fetchMock.mock('http://mocked.com', 201); - const res = await fetchMock.fetchHandler('http://unmocked.com'); - expect(res.status).toEqual(202); - fetchMock.restore(); - fetchMock.config.fallbackToNetwork = false; - delete globalThis.fetch; - }); - - it('actually falls back to network when configured in a sandbox properly', async () => { - const sbx = fm.sandbox(); - sbx.config.fetch = async () => ({ status: 202 }); //eslint-disable-line require-await - sbx.config.fallbackToNetwork = true; - sbx.mock('http://mocked.com', 201); - const res = await sbx('http://unmocked.com'); - expect(res.status).toEqual(202); - }); - - it('calls fetch with original Request object', async () => { - const sbx = fm.sandbox(); - let calledWith; - //eslint-disable-next-line require-await - sbx.config.fetch = async (req) => { - calledWith = req; - return { status: 202 }; - }; - sbx.config.fallbackToNetwork = true; - sbx.mock('http://mocked.com', 201); - const req = new sbx.config.Request('http://unmocked.com'); - await sbx(req); - expect(calledWith).toEqual(req); - }); - - describe('always', () => { - it('ignores routes that are matched', async () => { - fm.realFetch = async () => ({ status: 202 }); //eslint-disable-line require-await - fm.config.fallbackToNetwork = 'always'; - - fm.mock('http://mocked.com', 201); - const res = await fm.fetchHandler('http://unmocked.com'); - expect(res.status).toEqual(202); - }); - - it('ignores routes that are not matched', async () => { - fm.realFetch = async () => ({ status: 202 }); //eslint-disable-line require-await - - fm.config.fallbackToNetwork = 'always'; - - fm.mock('http://mocked.com', 201); - const res = await fm.fetchHandler('http://unmocked.com'); - expect(res.status).toEqual(202); - }); - }); - - describe.skip('warnOnFallback', () => { - it('warn on fallback response by default', () => {}); //eslint-disable-line no-empty-function - it("don't warn on fallback response when configured false", () => {}); //eslint-disable-line no-empty-function - }); -}); - - - -// import { Readable, Writable } from 'stream'; -// describe('nodejs only tests', () => { -// describe('support for nodejs body types', () => { - - // // only works in node-fetch@2 // it.skip('can respond with a readable stream', () => @@ -125,9 +39,17 @@ describe('fallbackToNetwork', () => { // .then((res) => res.blob()); // }); +describe('mock and spy', () => { + describe('.mockGlobal()', () => { + }) + describe('.spyGlobal()', () => { + + }) + +}) // describe.skip('client-side only tests', () => { // it('not throw when passing unmatched calls through to native fetch', () => { // fetchMock.config.fallbackToNetwork = true; @@ -152,10 +74,7 @@ describe('fallbackToNetwork', () => { // } // }); -// // in the browser the fetch spec disallows invoking res.headers on an -// // object that inherits from a response, thus breaking the ability to -// // read headers of a fake redirected response. -// if (typeof window === 'undefined') { + // it('not convert if `redirectUrl` property exists', async () => { // fm.route('*', { // redirectUrl: 'http://url.to.hit', @@ -163,7 +82,6 @@ describe('fallbackToNetwork', () => { // const res = await fm.fetchHandler('http://a.com/'); // expect(res.headers.get('content-type')).toBeNull(); // }); -// }