Skip to content

Commit

Permalink
feat: allow spying on just one route
Browse files Browse the repository at this point in the history
  • Loading branch information
wheresrhys committed Jul 25, 2024
1 parent bc4457d commit a9638fc
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 96 deletions.
29 changes: 24 additions & 5 deletions docs/docs/@fetch-mock/core/mocking-and-spying.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 4 additions & 0 deletions docs/docs/@fetch-mock/core/resetting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions packages/core/src/FetchMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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);
}
Expand Down
100 changes: 9 additions & 91 deletions packages/core/src/__tests__/FetchMock/mock-and-spy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () =>
Expand Down Expand Up @@ -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;
Expand All @@ -152,18 +74,14 @@ 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',
// });
// const res = await fm.fetchHandler('http://a.com/');
// expect(res.headers.get('content-type')).toBeNull();
// });
// }



Expand Down

0 comments on commit a9638fc

Please sign in to comment.