From 804f885c16a42a7534ee9fac2e103022482e2af6 Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Sat, 3 Aug 2024 20:33:09 +0100 Subject: [PATCH] feat: start implementing jest and vitest wrappers --- package-lock.json | 13 + packages/jest/README.md | 129 +++++++++ .../jest/__tests__/jest-built-ins.spec.js | 176 ++++++++++++ .../__tests__/jest-extensions-delete.spec.js | 251 +++++++++++++++++ .../__tests__/jest-extensions-get.spec.js | 251 +++++++++++++++++ .../__tests__/jest-extensions-head.spec.js | 255 +++++++++++++++++ .../__tests__/jest-extensions-patch.spec.js | 251 +++++++++++++++++ .../__tests__/jest-extensions-post.spec.js | 251 +++++++++++++++++ .../__tests__/jest-extensions-put.spec.js | 251 +++++++++++++++++ .../jest/__tests__/jest-extensions.spec.js | 258 ++++++++++++++++++ packages/jest/__tests__/regressions.spec.js | 20 ++ packages/jest/index.d.ts | 27 ++ packages/jest/jest-extensions.js | 110 ++++++++ packages/jest/jestify.js | 114 ++++++++ packages/vitest/package.json | 22 ++ packages/vitest/rollup.config.mjs | 2 + packages/vitest/src/index.js | 44 +++ packages/vitest/src/package.json | 1 + packages/vitest/src/vitest-extensions.js | 100 +++++++ 19 files changed, 2526 insertions(+) create mode 100644 packages/jest/README.md create mode 100644 packages/jest/__tests__/jest-built-ins.spec.js create mode 100644 packages/jest/__tests__/jest-extensions-delete.spec.js create mode 100644 packages/jest/__tests__/jest-extensions-get.spec.js create mode 100644 packages/jest/__tests__/jest-extensions-head.spec.js create mode 100644 packages/jest/__tests__/jest-extensions-patch.spec.js create mode 100644 packages/jest/__tests__/jest-extensions-post.spec.js create mode 100644 packages/jest/__tests__/jest-extensions-put.spec.js create mode 100644 packages/jest/__tests__/jest-extensions.spec.js create mode 100644 packages/jest/__tests__/regressions.spec.js create mode 100644 packages/jest/index.d.ts create mode 100644 packages/jest/jest-extensions.js create mode 100644 packages/jest/jestify.js create mode 100644 packages/vitest/package.json create mode 100644 packages/vitest/rollup.config.mjs create mode 100644 packages/vitest/src/index.js create mode 100644 packages/vitest/src/package.json create mode 100644 packages/vitest/src/vitest-extensions.js diff --git a/package-lock.json b/package-lock.json index f22b7c15..323b8ad1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,6 +69,7 @@ } }, "import-compat": { + "name": "fetch-mock-compat-tests", "version": "1.0.0" }, "node_modules/@algolia/autocomplete-core": { @@ -4038,6 +4039,10 @@ "resolved": "packages/core", "link": true }, + "node_modules/@fetch-mock/vitest": { + "resolved": "packages/vitest", + "link": true + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -27356,6 +27361,14 @@ } } }, + "packages/vitest": { + "name": "@fetch-mock/vitest", + "version": "0.0.0", + "license": "ISC", + "dependencies": { + "@fetch-mock/core": "^0.4.8" + } + }, "tool-compat": { "version": "1.0.0", "extraneous": true diff --git a/packages/jest/README.md b/packages/jest/README.md new file mode 100644 index 00000000..8497eaa1 --- /dev/null +++ b/packages/jest/README.md @@ -0,0 +1,129 @@ +# fetch-mock-jest + +Wrapper around [fetch-mock](http://www.wheresrhys.co.uk/fetch-mock) - a comprehensive, isomorphic mock for the [fetch api](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) - which provides an interface that is more idiomatic when working in [jest](https://jestjs.io). + +The example at the bottom of this readme demonstrates the intuitive API, but shows off only a fraction of fetch-mock's functionality. Features include: + +- mocks most of the fetch API spec, even advanced behaviours such as streaming and aborting +- declarative matching for most aspects of a http request, including url, headers, body and query parameters +- shorthands for the most commonly used features, such as matching a http method or matching one fetch only +- support for delaying responses, or using your own async functions to define custom race conditions +- can be used as a spy to observe real network requests +- isomorphic, and supports either a global fetch instance or a locally required instanceg + +# Requirements + +fetch-mock-jest requires the following to run: + +- [Node.js](https://Node.js.org/) 8+ for full feature operation +- [Node.js](https://Node.js.org/) 0.12+ with [limitations](http://www.wheresrhys.co.uk/fetch-mock/installation) +- [npm](https://www.npmjs.com/package/npm) (normally comes with Node.js) +- [jest](https://www.npmjs.com/package/jest) 25+ (may work with earlier versions, but untested) +- Either + - [node-fetch](https://www.npmjs.com/package/node-fetch) when testing in Node.js. To allow users a choice over which version to use, `node-fetch` is not included as a dependency of `fetch-mock`. + - A browser that supports the `fetch` API either natively or via a [polyfill/ponyfill](https://ponyfoo.com/articles/polyfills-or-ponyfills) + +# Installation + +`npm install -D fetch-mock-jest` + +## global fetch + +`const fetchMock = require('fetch-mock-jest')` + +## node-fetch + +``` +jest.mock('node-fetch', () => require('fetch-mock-jest').sandbox()) +const fetchMock = require('node-fetch') +``` + +# API + +## Setting up mocks + +Please refer to the [fetch-mock documentation](http://wheresrhys.co.uk/fetch-mock) and [cheatsheet](https://github.com/wheresrhys/fetch-mock/blob/master/docs/cheatsheet.md) + +All jest methods for configuring mock functions are disabled as fetch-mock's own methods should always be used + +## Inspecting mocks + +All the built in jest function inspection assertions can be used, e.g. `expect(fetchMock).toHaveBeenCalledWith('http://example.com')`. + +`fetchMock.mock.calls` and `fetchMock.mock.results` are also exposed, giving access to manually inspect the calls. + +The following custom jest expectation methods, proxying through to `fetch-mock`'s inspection methods are also available. They can all be prefixed with the `.not` helper for negative assertions. + +- `expect(fetchMock).toHaveFetched(filter, options)` +- `expect(fetchMock).toHaveLastFetched(filter, options)` +- `expect(fetchMock).toHaveNthFetched(n, filter, options)` +- `expect(fetchMock).toHaveFetchedTimes(n, filter, options)` +- `expect(fetchMock).toBeDone(filter)` + +### Notes + +- `filter` and `options` are the same as those used by [`fetch-mock`'s inspection methods](http://www.wheresrhys.co.uk/fetch-mock/#api-inspectionfundamentals) +- The obove methods can have `Fetched` replaced by any of the following verbs to scope to a particular method: + Got + Posted + Put + Deleted + FetchedHead + Patched + +e.g. `expect(fetchMock).toHaveLastPatched(filter, options)` + +## Tearing down mocks + +`fetchMock.mockClear()` can be used to reset the call history + +`fetchMock.mockReset()` can be used to remove all configured mocks + +Please report any bugs in resetting mocks on the [issues board](https://github.com/wheresrhys/fetch-mock-jest/issues) + +# Example + +```js +const fetchMock = require('fetch-mock-jest'); +const userManager = require('../src/user-manager'); + +test(async () => { + const users = [{ name: 'bob' }]; + fetchMock + .get('http://example.com/users', users) + .post('http://example.com/user', (url, options) => { + if (typeof options.body.name === 'string') { + users.push(options.body); + return 202; + } + return 400; + }) + .patch( + { + url: 'http://example.com/user' + }, + 405 + ); + + expect(await userManager.getAll()).toEqual([{ name: 'bob' }]); + expect(fetchMock).toHaveLastFetched('http://example.com/users + get'); + await userManager.create({ name: true }); + expect(fetchMock).toHaveLastFetched( + { + url: 'http://example.com/user', + body: { name: true } + }, + 'post' + ); + expect(await userManager.getAll()).toEqual([{ name: 'bob' }]); + fetchMock.mockClear(); + await userManager.create({ name: 'sarah' }); + expect(fetchMock).toHaveLastFetched( + { + url: 'http://example.com/user', + body: { name: 'sarah' } + }, + 'post' + ); + expect(await userManager.getAll()).toEqual([ + { name: 'bob' }, + { name: 'sarah' } + ]); + fetchMock.mockReset(); +}); +``` diff --git a/packages/jest/__tests__/jest-built-ins.spec.js b/packages/jest/__tests__/jest-built-ins.spec.js new file mode 100644 index 00000000..35c05d96 --- /dev/null +++ b/packages/jest/__tests__/jest-built-ins.spec.js @@ -0,0 +1,176 @@ +/*global jest, beforeAll, afterAll */ +jest.mock('node-fetch', () => require('../server').sandbox()); +const fetch = require('node-fetch'); +describe('jest built-ins', () => { + describe('exposing mock internals', () => { + beforeAll(() => { + fetch.mock('http://example.com', 200).mock('http://example2.com', 201); + fetch('http://example.com', { + headers: { + test: 'header', + }, + }); + fetch('http://example2.com', { + headers: { + test: 'header2', + }, + }); + }); + + afterAll(() => fetch.reset()); + it('exposes `calls` property', () => { + expect(fetch.mock.calls).toBeDefined(); + expect(fetch.mock.calls.length).toBe(2); + expect(fetch.mock.calls).toMatchObject([ + [ + 'http://example.com', + { + headers: { + test: 'header', + }, + }, + ], + [ + 'http://example2.com', + { + headers: { + test: 'header2', + }, + }, + ], + ]); + }); + it('exposes `results` property', async () => { + expect(fetch.mock.results).toBeDefined(); + expect(fetch.mock.results.length).toEqual(2); + expect(await fetch.mock.results[0].value).toMatchObject({ + status: 200, + }); + expect(await fetch.mock.results[1].value).toMatchObject({ + status: 201, + }); + }); + }); + + describe('clearing', () => { + beforeEach(() => { + fetch.mock('http://example.com', 200).mock('http://example2.com', 201); + fetch('http://example.com', { + headers: { + test: 'header', + }, + }); + fetch('http://example2.com', { + headers: { + test: 'header2', + }, + }); + }); + + afterEach(() => fetch.reset()); + it('mockClear', () => { + expect(fetch.mockClear).toBeDefined(); + fetch.mockClear(); + expect(fetch.mock.calls.length).toEqual(0); + expect(fetch._calls.length).toEqual(0); + expect(fetch.routes.length).toEqual(2); + }); + it('mockReset', () => { + expect(fetch.mockReset).toBeDefined(); + fetch.mockReset(); + expect(fetch.mock.calls.length).toEqual(0); + expect(fetch._calls.length).toEqual(0); + expect(fetch.routes.length).toEqual(0); + }); + it('mockRestore', () => { + expect(() => fetch.mockRestore()).toThrow( + "mockRestore not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }); + it('mockImplementation', () => { + expect(() => fetch.mockImplementation()).toThrow( + "mockImplementation not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }); + it('mockImplementationOnce', () => { + expect(() => fetch.mockImplementationOnce()).toThrow( + "mockImplementationOnce not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }); + it('mockName', () => { + expect(() => fetch.mockName()).toThrow( + "mockName not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }); + it('mockReturnThis', () => { + expect(() => fetch.mockReturnThis()).toThrow( + "mockReturnThis not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }); + it('mockReturnValue', () => { + expect(() => fetch.mockReturnValue()).toThrow( + "mockReturnValue not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }); + it('mockReturnValueOnce', () => { + expect(() => fetch.mockReturnValueOnce()).toThrow( + "mockReturnValueOnce not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }); + it('mockResolvedValue', () => { + expect(() => fetch.mockResolvedValue()).toThrow( + "mockResolvedValue not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }); + it('mockResolvedValueOnce', () => { + expect(() => fetch.mockResolvedValueOnce()).toThrow( + "mockResolvedValueOnce not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }); + it('mockRejectedValue', () => { + expect(() => fetch.mockRejectedValue()).toThrow( + "mockRejectedValue not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }); + it('mockRejectedValueOnce', () => { + expect(() => fetch.mockRejectedValueOnce()).toThrow( + "mockRejectedValueOnce not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }); + }); + describe('native jest mock function inspectors', () => { + it('.toHaveBeenCalled()', () => { + expect(() => expect(fetch).toHaveBeenCalled()).not.toThrow(); + }); + // Just want to get the fix out for calling fetch methods + // These will all work as the basic mechanism is fixed, but + // no time to set up all th etest cases now + it.skip('.toHaveBeenCalledTimes(number)', () => { + expect(() => expect(fetch).toHaveBeenCalledTimes(1)).not.toThrow(); + }); + it.skip('.toHaveBeenCalledWith(arg1, arg2, ...)', () => { + expect(() => expect(fetch).toHaveBeenCalledWith(1)).not.toThrow(); + }); + it.skip('.toHaveBeenLastCalledWith(arg1, arg2, ...)', () => { + expect(() => expect(fetch).toHaveBeenLastCalledWith(1)).not.toThrow(); + }); + it.skip('.toHaveBeenNthCalledWith(nthCall, arg1, arg2, ....)', () => { + expect(() => expect(fetch).toHaveBeenNthCalledWith(1, 1)).not.toThrow(); + }); + it.skip('.toHaveReturned()', () => { + expect(() => expect(fetch).toHaveReturned()).not.toThrow(); + }); + it.skip('.toHaveReturnedTimes(number)', () => { + expect(() => expect(fetch).toHaveReturnedTimes(1)).not.toThrow(); + }); + it.skip('.toHaveReturnedWith(value)', () => { + expect(() => expect(fetch).toHaveReturnedWith(1)).not.toThrow(); + }); + it.skip('.toHaveLastReturnedWith(value)', () => { + expect(() => expect(fetch).toHaveLastReturnedWith(1)).not.toThrow(); + }); + it.skip('.toHaveNthReturnedWith(nthCall, value)', () => { + expect(() => expect(fetch).toHaveNthReturnedWith(1, 1)).not.toThrow(); + }); + }); +}); diff --git a/packages/jest/__tests__/jest-extensions-delete.spec.js b/packages/jest/__tests__/jest-extensions-delete.spec.js new file mode 100644 index 00000000..fa3fceaa --- /dev/null +++ b/packages/jest/__tests__/jest-extensions-delete.spec.js @@ -0,0 +1,251 @@ +/*global jest, beforeAll, afterAll */ +jest.mock('node-fetch', () => require('../server').sandbox()); +const fetch = require('node-fetch'); +describe('jest extensions - delete', () => { + describe('when no calls', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://lala'); + }); + afterAll(() => fetch.reset()); + it('toHaveDeleted should be falsy', () => { + expect(fetch).not.toHaveDeleted('http://example.com/path'); + }); + + it('toHaveLastDeleted should be falsy', () => { + expect(fetch).not.toHaveLastDeleted('http://example.com/path'); + }); + + it('toHaveNthDeleted should be falsy', () => { + expect(fetch).not.toHaveNthDeleted(1, 'http://example.com/path'); + }); + + it('toHaveDeletedTimes should be falsy', () => { + expect(fetch).not.toHaveDeletedTimes(1, 'http://example.com/path'); + }); + + it('toBeDone should be falsy', () => { + expect(fetch).not.toBeDone('http://example.com/path'); + }); + }); + describe('toHaveDeleted', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path2', { + method: 'delete', + headers: { + test: 'header', + }, + }); + fetch('http://example.com/path', { + method: 'delete', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveDeleted('http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveDeleted('begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveDeleted('http://example.com/path', { + method: 'delete', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveDeleted('http://example.com/path', { + method: 'delete', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveDeleted('http://example-no.com/path', { + method: 'delete', + headers: { + test: 'header', + }, + }); + }); + }); + describe('toHaveLastDeleted', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path', { + method: 'delete', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveLastDeleted('http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveLastDeleted('begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveLastDeleted('http://example.com/path', { + method: 'delete', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveLastDeleted('http://example.com/path', { + method: 'delete', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveLastDeleted('http://example-no.com/path', { + method: 'delete', + headers: { + test: 'header', + }, + }); + }); + }); + + describe('toHaveNthDeleted', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example1.com/path', { + method: 'delete', + headers: { + test: 'header', + }, + }); + fetch('http://example2.com/path', { + method: 'delete', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveNthDeleted(2, 'http://example2.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveNthDeleted(2, 'begin:http://example2.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveNthDeleted(2, 'http://example2.com/path', { + method: 'delete', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveNthDeleted(2, 'http://example2.com/path', { + method: 'delete', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveNthDeleted(2, 'http://example-no.com/path', { + method: 'delete', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if wrong n", () => { + expect(fetch).not.toHaveNthDeleted(1, 'http://example2.com/path'); + }); + }); + + describe('toHaveDeletedTimes', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path', { + method: 'delete', + headers: { + test: 'header', + }, + }); + fetch('http://example.com/path', { + method: 'delete', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveDeletedTimes(2, 'http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveDeletedTimes(2, 'begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveDeletedTimes(2, 'http://example.com/path', { + method: 'delete', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveDeletedTimes(2, 'http://example.com/path', { + method: 'delete', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveDeletedTimes(2, 'http://example-no.com/path', { + method: 'delete', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if too few calls", () => { + expect(fetch).not.toHaveDeletedTimes(1, 'http://example.com/path'); + }); + + it("doesn't match if too many calls", () => { + expect(fetch).not.toHaveDeletedTimes(3, 'http://example.com/path'); + }); + }); +}); diff --git a/packages/jest/__tests__/jest-extensions-get.spec.js b/packages/jest/__tests__/jest-extensions-get.spec.js new file mode 100644 index 00000000..72875fe7 --- /dev/null +++ b/packages/jest/__tests__/jest-extensions-get.spec.js @@ -0,0 +1,251 @@ +/*global jest, beforeAll, afterAll */ +jest.mock('node-fetch', () => require('../server').sandbox()); +const fetch = require('node-fetch'); +describe('jest extensions - get', () => { + describe('when no calls', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://lala', { method: 'post' }); + }); + afterAll(() => fetch.reset()); + it('toHaveGot should be falsy', () => { + expect(fetch).not.toHaveGot('http://example.com/path'); + }); + + it('toHaveLastGot should be falsy', () => { + expect(fetch).not.toHaveLastGot('http://example.com/path'); + }); + + it('toHaveNthGot should be falsy', () => { + expect(fetch).not.toHaveNthGot(1, 'http://example.com/path'); + }); + + it('toHaveGotTimes should be falsy', () => { + expect(fetch).not.toHaveGotTimes(1, 'http://example.com/path'); + }); + + it('toBeDone should be falsy', () => { + expect(fetch).not.toBeDone('http://example.com/path'); + }); + }); + describe('toHaveGot', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path2', { + method: 'get', + headers: { + test: 'header', + }, + }); + fetch('http://example.com/path', { + method: 'get', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveGot('http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveGot('begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveGot('http://example.com/path', { + method: 'get', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveGot('http://example.com/path', { + method: 'get', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveGot('http://example-no.com/path', { + method: 'get', + headers: { + test: 'header', + }, + }); + }); + }); + describe('toHaveLastGot', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path', { + method: 'get', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveLastGot('http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveLastGot('begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveLastGot('http://example.com/path', { + method: 'get', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveLastGot('http://example.com/path', { + method: 'get', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveLastGot('http://example-no.com/path', { + method: 'get', + headers: { + test: 'header', + }, + }); + }); + }); + + describe('toHaveNthGot', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example1.com/path', { + method: 'get', + headers: { + test: 'header', + }, + }); + fetch('http://example2.com/path', { + method: 'get', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveNthGot(2, 'http://example2.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveNthGot(2, 'begin:http://example2.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveNthGot(2, 'http://example2.com/path', { + method: 'get', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveNthGot(2, 'http://example2.com/path', { + method: 'get', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveNthGot(2, 'http://example-no.com/path', { + method: 'get', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if wrong n", () => { + expect(fetch).not.toHaveNthGot(1, 'http://example2.com/path'); + }); + }); + + describe('toHaveGotTimes', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path', { + method: 'get', + headers: { + test: 'header', + }, + }); + fetch('http://example.com/path', { + method: 'get', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveGotTimes(2, 'http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveGotTimes(2, 'begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveGotTimes(2, 'http://example.com/path', { + method: 'get', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveGotTimes(2, 'http://example.com/path', { + method: 'get', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveGotTimes(2, 'http://example-no.com/path', { + method: 'get', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if too few calls", () => { + expect(fetch).not.toHaveGotTimes(1, 'http://example.com/path'); + }); + + it("doesn't match if too many calls", () => { + expect(fetch).not.toHaveGotTimes(3, 'http://example.com/path'); + }); + }); +}); diff --git a/packages/jest/__tests__/jest-extensions-head.spec.js b/packages/jest/__tests__/jest-extensions-head.spec.js new file mode 100644 index 00000000..01409551 --- /dev/null +++ b/packages/jest/__tests__/jest-extensions-head.spec.js @@ -0,0 +1,255 @@ +/*global jest, beforeAll, afterAll */ +jest.mock('node-fetch', () => require('../server').sandbox()); +const fetch = require('node-fetch'); +describe('jest extensions - head', () => { + describe('when no calls', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://lala'); + }); + afterAll(() => fetch.reset()); + it('toHaveFetchedHead should be falsy', () => { + expect(fetch).not.toHaveFetchedHead('http://example.com/path'); + }); + + it('toHaveLastFetchedHead should be falsy', () => { + expect(fetch).not.toHaveLastFetchedHead('http://example.com/path'); + }); + + it('toHaveNthFetchedHead should be falsy', () => { + expect(fetch).not.toHaveNthFetchedHead(1, 'http://example.com/path'); + }); + + it('toHaveFetchedHeadTimes should be falsy', () => { + expect(fetch).not.toHaveFetchedHeadTimes(1, 'http://example.com/path'); + }); + + it('toBeDone should be falsy', () => { + expect(fetch).not.toBeDone('http://example.com/path'); + }); + }); + describe('toHaveFetchedHead', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path2', { + method: 'head', + headers: { + test: 'header', + }, + }); + fetch('http://example.com/path', { + method: 'head', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveFetchedHead('http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveFetchedHead('begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveFetchedHead('http://example.com/path', { + method: 'head', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveFetchedHead('http://example.com/path', { + method: 'head', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveFetchedHead('http://example-no.com/path', { + method: 'head', + headers: { + test: 'header', + }, + }); + }); + }); + describe('toHaveLastFetchedHead', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path', { + method: 'head', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveLastFetchedHead('http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveLastFetchedHead('begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveLastFetchedHead('http://example.com/path', { + method: 'head', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveLastFetchedHead('http://example.com/path', { + method: 'head', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveLastFetchedHead('http://example-no.com/path', { + method: 'head', + headers: { + test: 'header', + }, + }); + }); + }); + + describe('toHaveNthFetchedHead', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example1.com/path', { + method: 'head', + headers: { + test: 'header', + }, + }); + fetch('http://example2.com/path', { + method: 'head', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveNthFetchedHead(2, 'http://example2.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveNthFetchedHead(2, 'begin:http://example2.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveNthFetchedHead(2, 'http://example2.com/path', { + method: 'head', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveNthFetchedHead(2, 'http://example2.com/path', { + method: 'head', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveNthFetchedHead(2, 'http://example-no.com/path', { + method: 'head', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if wrong n", () => { + expect(fetch).not.toHaveNthFetchedHead(1, 'http://example2.com/path'); + }); + }); + + describe('toHaveFetchedHeadTimes', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path', { + method: 'head', + headers: { + test: 'header', + }, + }); + fetch('http://example.com/path', { + method: 'head', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveFetchedHeadTimes(2, 'http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveFetchedHeadTimes(2, 'begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveFetchedHeadTimes(2, 'http://example.com/path', { + method: 'head', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveFetchedHeadTimes(2, 'http://example.com/path', { + method: 'head', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveFetchedHeadTimes( + 2, + 'http://example-no.com/path', + { + method: 'head', + headers: { + test: 'header', + }, + } + ); + }); + + it("doesn't match if too few calls", () => { + expect(fetch).not.toHaveFetchedHeadTimes(1, 'http://example.com/path'); + }); + + it("doesn't match if too many calls", () => { + expect(fetch).not.toHaveFetchedHeadTimes(3, 'http://example.com/path'); + }); + }); +}); diff --git a/packages/jest/__tests__/jest-extensions-patch.spec.js b/packages/jest/__tests__/jest-extensions-patch.spec.js new file mode 100644 index 00000000..54d9ac9e --- /dev/null +++ b/packages/jest/__tests__/jest-extensions-patch.spec.js @@ -0,0 +1,251 @@ +/*global jest, beforeAll, afterAll */ +jest.mock('node-fetch', () => require('../server').sandbox()); +const fetch = require('node-fetch'); +describe('jest extensions - patch', () => { + describe('when no calls', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://lala'); + }); + afterAll(() => fetch.reset()); + it('toHavePatched should be falsy', () => { + expect(fetch).not.toHavePatched('http://example.com/path'); + }); + + it('toHaveLastPatched should be falsy', () => { + expect(fetch).not.toHaveLastPatched('http://example.com/path'); + }); + + it('toHaveNthPatched should be falsy', () => { + expect(fetch).not.toHaveNthPatched(1, 'http://example.com/path'); + }); + + it('toHavePatchedTimes should be falsy', () => { + expect(fetch).not.toHavePatchedTimes(1, 'http://example.com/path'); + }); + + it('toBeDone should be falsy', () => { + expect(fetch).not.toBeDone('http://example.com/path'); + }); + }); + describe('toHavePatched', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path2', { + method: 'patch', + headers: { + test: 'header', + }, + }); + fetch('http://example.com/path', { + method: 'patch', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHavePatched('http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHavePatched('begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHavePatched('http://example.com/path', { + method: 'patch', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHavePatched('http://example.com/path', { + method: 'patch', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHavePatched('http://example-no.com/path', { + method: 'patch', + headers: { + test: 'header', + }, + }); + }); + }); + describe('toHaveLastPatched', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path', { + method: 'patch', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveLastPatched('http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveLastPatched('begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveLastPatched('http://example.com/path', { + method: 'patch', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveLastPatched('http://example.com/path', { + method: 'patch', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveLastPatched('http://example-no.com/path', { + method: 'patch', + headers: { + test: 'header', + }, + }); + }); + }); + + describe('toHaveNthPatched', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example1.com/path', { + method: 'patch', + headers: { + test: 'header', + }, + }); + fetch('http://example2.com/path', { + method: 'patch', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveNthPatched(2, 'http://example2.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveNthPatched(2, 'begin:http://example2.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveNthPatched(2, 'http://example2.com/path', { + method: 'patch', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveNthPatched(2, 'http://example2.com/path', { + method: 'patch', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveNthPatched(2, 'http://example-no.com/path', { + method: 'patch', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if wrong n", () => { + expect(fetch).not.toHaveNthPatched(1, 'http://example2.com/path'); + }); + }); + + describe('toHavePatchedTimes', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path', { + method: 'patch', + headers: { + test: 'header', + }, + }); + fetch('http://example.com/path', { + method: 'patch', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHavePatchedTimes(2, 'http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHavePatchedTimes(2, 'begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHavePatchedTimes(2, 'http://example.com/path', { + method: 'patch', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHavePatchedTimes(2, 'http://example.com/path', { + method: 'patch', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHavePatchedTimes(2, 'http://example-no.com/path', { + method: 'patch', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if too few calls", () => { + expect(fetch).not.toHavePatchedTimes(1, 'http://example.com/path'); + }); + + it("doesn't match if too many calls", () => { + expect(fetch).not.toHavePatchedTimes(3, 'http://example.com/path'); + }); + }); +}); diff --git a/packages/jest/__tests__/jest-extensions-post.spec.js b/packages/jest/__tests__/jest-extensions-post.spec.js new file mode 100644 index 00000000..09d8cd06 --- /dev/null +++ b/packages/jest/__tests__/jest-extensions-post.spec.js @@ -0,0 +1,251 @@ +/*global jest, beforeAll, afterAll */ +jest.mock('node-fetch', () => require('../server').sandbox()); +const fetch = require('node-fetch'); +describe('jest extensions - post', () => { + describe('when no calls', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://lala'); + }); + afterAll(() => fetch.reset()); + it('toHavePosted should be falsy', () => { + expect(fetch).not.toHavePosted('http://example.com/path'); + }); + + it('toHaveLastPosted should be falsy', () => { + expect(fetch).not.toHaveLastPosted('http://example.com/path'); + }); + + it('toHaveNthPosted should be falsy', () => { + expect(fetch).not.toHaveNthPosted(1, 'http://example.com/path'); + }); + + it('toHavePostedTimes should be falsy', () => { + expect(fetch).not.toHavePostedTimes(1, 'http://example.com/path'); + }); + + it('toBeDone should be falsy', () => { + expect(fetch).not.toBeDone('http://example.com/path'); + }); + }); + describe('toHavePosted', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path2', { + method: 'post', + headers: { + test: 'header', + }, + }); + fetch('http://example.com/path', { + method: 'post', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHavePosted('http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHavePosted('begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHavePosted('http://example.com/path', { + method: 'post', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHavePosted('http://example.com/path', { + method: 'post', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHavePosted('http://example-no.com/path', { + method: 'post', + headers: { + test: 'header', + }, + }); + }); + }); + describe('toHaveLastPosted', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path', { + method: 'post', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveLastPosted('http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveLastPosted('begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveLastPosted('http://example.com/path', { + method: 'post', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveLastPosted('http://example.com/path', { + method: 'post', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveLastPosted('http://example-no.com/path', { + method: 'post', + headers: { + test: 'header', + }, + }); + }); + }); + + describe('toHaveNthPosted', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example1.com/path', { + method: 'post', + headers: { + test: 'header', + }, + }); + fetch('http://example2.com/path', { + method: 'post', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveNthPosted(2, 'http://example2.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveNthPosted(2, 'begin:http://example2.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveNthPosted(2, 'http://example2.com/path', { + method: 'post', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveNthPosted(2, 'http://example2.com/path', { + method: 'post', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveNthPosted(2, 'http://example-no.com/path', { + method: 'post', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if wrong n", () => { + expect(fetch).not.toHaveNthPosted(1, 'http://example2.com/path'); + }); + }); + + describe('toHavePostedTimes', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path', { + method: 'post', + headers: { + test: 'header', + }, + }); + fetch('http://example.com/path', { + method: 'post', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHavePostedTimes(2, 'http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHavePostedTimes(2, 'begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHavePostedTimes(2, 'http://example.com/path', { + method: 'post', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHavePostedTimes(2, 'http://example.com/path', { + method: 'post', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHavePostedTimes(2, 'http://example-no.com/path', { + method: 'post', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if too few calls", () => { + expect(fetch).not.toHavePostedTimes(1, 'http://example.com/path'); + }); + + it("doesn't match if too many calls", () => { + expect(fetch).not.toHavePostedTimes(3, 'http://example.com/path'); + }); + }); +}); diff --git a/packages/jest/__tests__/jest-extensions-put.spec.js b/packages/jest/__tests__/jest-extensions-put.spec.js new file mode 100644 index 00000000..3d5cd09b --- /dev/null +++ b/packages/jest/__tests__/jest-extensions-put.spec.js @@ -0,0 +1,251 @@ +/*global jest, beforeAll, afterAll */ +jest.mock('node-fetch', () => require('../server').sandbox()); +const fetch = require('node-fetch'); +describe('jest extensions - put', () => { + describe('when no calls', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://lala'); + }); + afterAll(() => fetch.reset()); + it('toHavePut should be falsy', () => { + expect(fetch).not.toHavePut('http://example.com/path'); + }); + + it('toHaveLastPut should be falsy', () => { + expect(fetch).not.toHaveLastPut('http://example.com/path'); + }); + + it('toHaveNthPut should be falsy', () => { + expect(fetch).not.toHaveNthPut(1, 'http://example.com/path'); + }); + + it('toHavePutTimes should be falsy', () => { + expect(fetch).not.toHavePutTimes(1, 'http://example.com/path'); + }); + + it('toBeDone should be falsy', () => { + expect(fetch).not.toBeDone('http://example.com/path'); + }); + }); + describe('toHavePut', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path2', { + method: 'put', + headers: { + test: 'header', + }, + }); + fetch('http://example.com/path', { + method: 'put', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHavePut('http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHavePut('begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHavePut('http://example.com/path', { + method: 'put', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHavePut('http://example.com/path', { + method: 'put', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHavePut('http://example-no.com/path', { + method: 'put', + headers: { + test: 'header', + }, + }); + }); + }); + describe('toHaveLastPut', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path', { + method: 'put', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveLastPut('http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveLastPut('begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveLastPut('http://example.com/path', { + method: 'put', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveLastPut('http://example.com/path', { + method: 'put', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveLastPut('http://example-no.com/path', { + method: 'put', + headers: { + test: 'header', + }, + }); + }); + }); + + describe('toHaveNthPut', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example1.com/path', { + method: 'put', + headers: { + test: 'header', + }, + }); + fetch('http://example2.com/path', { + method: 'put', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveNthPut(2, 'http://example2.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveNthPut(2, 'begin:http://example2.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveNthPut(2, 'http://example2.com/path', { + method: 'put', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveNthPut(2, 'http://example2.com/path', { + method: 'put', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveNthPut(2, 'http://example-no.com/path', { + method: 'put', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if wrong n", () => { + expect(fetch).not.toHaveNthPut(1, 'http://example2.com/path'); + }); + }); + + describe('toHavePutTimes', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path', { + method: 'put', + headers: { + test: 'header', + }, + }); + fetch('http://example.com/path', { + method: 'put', + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHavePutTimes(2, 'http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHavePutTimes(2, 'begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHavePutTimes(2, 'http://example.com/path', { + method: 'put', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHavePutTimes(2, 'http://example.com/path', { + method: 'put', + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHavePutTimes(2, 'http://example-no.com/path', { + method: 'put', + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if too few calls", () => { + expect(fetch).not.toHavePutTimes(1, 'http://example.com/path'); + }); + + it("doesn't match if too many calls", () => { + expect(fetch).not.toHavePutTimes(3, 'http://example.com/path'); + }); + }); +}); diff --git a/packages/jest/__tests__/jest-extensions.spec.js b/packages/jest/__tests__/jest-extensions.spec.js new file mode 100644 index 00000000..8fb65f0a --- /dev/null +++ b/packages/jest/__tests__/jest-extensions.spec.js @@ -0,0 +1,258 @@ +/*global jest, beforeAll, afterAll */ +jest.mock('node-fetch', () => require('../server').sandbox()); +const fetch = require('node-fetch'); +describe('jest extensions', () => { + describe('when no calls', () => { + beforeAll(() => { + fetch.mock('*', 200); + }); + afterAll(() => fetch.reset()); + it('toHaveFetched should be falsy', () => { + expect(fetch).not.toHaveFetched('http://example.com/path'); + }); + + it('toHaveLastFetched should be falsy', () => { + expect(fetch).not.toHaveLastFetched('http://example.com/path'); + }); + + it('toHaveNthFetched should be falsy', () => { + expect(fetch).not.toHaveNthFetched(1, 'http://example.com/path'); + }); + + it('toHaveFetchedTimes should be falsy', () => { + expect(fetch).not.toHaveFetchedTimes(1, 'http://example.com/path'); + }); + + it('toBeDone should be falsy', () => { + expect(fetch).not.toBeDone(); + expect(fetch).not.toBeDone('http://example.com/path'); + }); + }); + describe('toHaveFetched', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path2', { + headers: { + test: 'header', + }, + }); + fetch('http://example.com/path', { + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveFetched('http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveFetched('begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveFetched('http://example.com/path', { + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveFetched('http://example.com/path', { + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveFetched('http://example-no.com/path', { + headers: { + test: 'header', + }, + }); + }); + }); + describe('toHaveLastFetched', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path', { + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveLastFetched('http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveLastFetched('begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveLastFetched('http://example.com/path', { + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveLastFetched('http://example.com/path', { + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveLastFetched('http://example-no.com/path', { + headers: { + test: 'header', + }, + }); + }); + }); + + describe('toHaveNthFetched', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example1.com/path', { + headers: { + test: 'header', + }, + }); + fetch('http://example2.com/path', { + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveNthFetched(2, 'http://example2.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveNthFetched(2, 'begin:http://example2.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveNthFetched(2, 'http://example2.com/path', { + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveNthFetched(2, 'http://example2.com/path', { + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveNthFetched(2, 'http://example-no.com/path', { + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if wrong n", () => { + expect(fetch).not.toHaveNthFetched(1, 'http://example2.com/path'); + }); + }); + + describe('toHaveFetchedTimes', () => { + beforeAll(() => { + fetch.mock('*', 200); + fetch('http://example.com/path', { + headers: { + test: 'header', + }, + }); + fetch('http://example.com/path', { + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toHaveFetchedTimes(2, 'http://example.com/path'); + }); + + it('matches with fetch-mock matcher', () => { + expect(fetch).toHaveFetchedTimes(2, 'begin:http://example.com/path'); + }); + + it('matches with matcher and options', () => { + expect(fetch).toHaveFetchedTimes(2, 'http://example.com/path', { + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if matcher but not options is correct", () => { + expect(fetch).not.toHaveFetchedTimes(2, 'http://example.com/path', { + headers: { + test: 'not-header', + }, + }); + }); + + it("doesn't match if options but not matcher is correct", () => { + expect(fetch).not.toHaveFetchedTimes(2, 'http://example-no.com/path', { + headers: { + test: 'header', + }, + }); + }); + + it("doesn't match if too few calls", () => { + expect(fetch).not.toHaveFetchedTimes(1, 'http://example.com/path'); + }); + + it("doesn't match if too many calls", () => { + expect(fetch).not.toHaveFetchedTimes(3, 'http://example.com/path'); + }); + }); + + describe('toBeDone', () => { + beforeAll(() => { + fetch.once('http://example.com/path', 200); + fetch.mock('http://example2.com/path', 200, { repeat: 2 }); + fetch('http://example.com/path', { + headers: { + test: 'header', + }, + }); + fetch('http://example2.com/path', { + headers: { + test: 'header', + }, + }); + }); + afterAll(() => fetch.reset()); + + it('matches with just url', () => { + expect(fetch).toBeDone('http://example.com/path'); + }); + + it("doesn't match if too few calls", () => { + expect(fetch).not.toBeDone('http://example2.com/path'); + }); + }); +}); diff --git a/packages/jest/__tests__/regressions.spec.js b/packages/jest/__tests__/regressions.spec.js new file mode 100644 index 00000000..21f74252 --- /dev/null +++ b/packages/jest/__tests__/regressions.spec.js @@ -0,0 +1,20 @@ +/*global jest */ +jest.mock('node-fetch', () => require('../server').sandbox()); +const fetch = require('node-fetch'); + +describe('regressions and strange cases', () => { + it('works even when jest.resetAllMocks() is called', () => { + jest.resetAllMocks(); + fetch.mock('*', 200); + fetch('http://example.com/path', 200); + expect(fetch).toHaveFetched('http://example.com/path'); + fetch.reset(); + }); + it('works even when jest.clearAllMocks() is called', () => { + jest.clearAllMocks(); + fetch.mock('*', 200); + fetch('http://example.com/path', 200); + expect(fetch).toHaveFetched('http://example.com/path'); + fetch.reset(); + }); +}); diff --git a/packages/jest/index.d.ts b/packages/jest/index.d.ts new file mode 100644 index 00000000..4d3c0a81 --- /dev/null +++ b/packages/jest/index.d.ts @@ -0,0 +1,27 @@ +import { InspectionFilter, InspectionOptions, FetchMockStatic, MockCall, FetchMockSandbox } from 'fetch-mock'; + +declare global { + namespace jest { + interface Matchers { + toHaveFetched(filter?: InspectionFilter, options?: InspectionOptions): R; + toHaveLastFetched(filter?: InspectionFilter, options?: InspectionOptions): R; + toHaveNthFetched(n: number, filter?: InspectionFilter, options?: InspectionOptions): R; + toHaveFetchedTimes(times: number, filter?: InspectionFilter, options?: InspectionOptions): R; + toBeDone(filter?: InspectionFilter): R; + } + } +} + + +interface FetchMockJest { + // Reset the call history + mockClear(): void; + // Remove all configured mocks + mockReset(): void; + // Enable sandbox mode + sandbox(): jest.MockInstance & FetchMockSandbox; +} + +declare const fetchMockJest: FetchMockJest & jest.MockInstance & FetchMockStatic + +export = fetchMockJest; diff --git a/packages/jest/jest-extensions.js b/packages/jest/jest-extensions.js new file mode 100644 index 00000000..659a0901 --- /dev/null +++ b/packages/jest/jest-extensions.js @@ -0,0 +1,110 @@ +const callsAreEqual = (c1, c2) => { + if (!c1 && !c2) return true; + if (!c1 || !c2) return false; + if (c1[0] !== c2[0]) return false; + if (c1[1] !== c2[1]) return false; + if (c1.request !== c2.request) return false; + if (c1.identifier !== c2.identifier) return false; + if (c1.isUnmatched !== c2.isUnmatched) return false; + if (c1.response !== c2.response) return false; + return true; +}; + +const methodlessExtensions = { + toHaveFetched: (fetchMock, url, options) => { + if (fetchMock.called(url, options)) { + return { pass: true }; + } + return { + pass: false, + message: () => `fetch should have been called with ${url}`, + }; + }, + toHaveLastFetched: (fetchMock, url, options) => { + const allCalls = fetchMock.calls(); + if (!allCalls.length) { + return { + pass: false, + message: () => `No calls made to fetch`, + }; + } + const lastCall = [...allCalls].pop(); + const lastUrlCall = [...fetchMock.calls(url, options)].pop(); + if (callsAreEqual(lastCall, lastUrlCall)) { + return { pass: true }; + } + return { + pass: false, + message: () => + `Last call to fetch should have had a URL of ${url} but was ${lastCall.url}`, + }; + }, + + toHaveNthFetched: (fetchMock, n, url, options) => { + const nthCall = fetchMock.calls()[n - 1]; + const urlCalls = fetchMock.calls(url, options); + if (urlCalls.some((call) => callsAreEqual(call, nthCall))) { + return { pass: true }; + } + return { + pass: false, + message: () => + `${n}th call to fetch should have had a URL of ${url} but was ${nthCall.url}`, + }; + }, + + toHaveFetchedTimes: (fetchMock, times, url, options) => { + const calls = fetchMock.calls(url, options); + if (calls.length === times) { + return { pass: true }; + } + return { + pass: false, + message: () => + `fetch should have been called with a URL of ${url} ${times} times, but it was called ${calls.length} times`, + }; + }, +}; + +expect.extend(methodlessExtensions); +expect.extend({ + toBeDone: (fetchMock, matcher) => { + const done = fetchMock.done(matcher); + if (done) { + return { pass: true }; + } + return { + pass: false, + message: () => + `fetch has not been called the expected number of times ${ + matcher ? `for ${matcher}` : 'in total' + }`, + }; + }, +}); + +[ + 'Got:get', + 'Posted:post', + 'Put:put', + 'Deleted:delete', + 'FetchedHead:head', + 'Patched:patch', +].forEach((verbs) => { + const [humanVerb, method] = verbs.split(':'); + + const extensions = Object.entries(methodlessExtensions) + .map(([name, func]) => { + return [ + (name = name.replace('Fetched', humanVerb)), + (...args) => { + const opts = args[func.length - 1] || {}; + args[func.length - 1] = { ...opts, method }; + return func(...args); + }, + ]; + }) + .reduce((obj, [name, func]) => ({ ...obj, [name]: func }), {}); + + expect.extend(extensions); +}); diff --git a/packages/jest/jestify.js b/packages/jest/jestify.js new file mode 100644 index 00000000..79107bf8 --- /dev/null +++ b/packages/jest/jestify.js @@ -0,0 +1,114 @@ +/*global jest*/ +require('./jest-extensions'); + +const jestify = (fetchMockInstance) => { + const jestifiedInstance = new Proxy(fetchMockInstance, { + get: (originalFetchMock, name) => { + if (name === 'sandbox') { + return new Proxy(originalFetchMock[name], { + apply: (func, thisArg, args) => { + const sandboxedFetchMock = func.apply(originalFetchMock, args); + return jestify(sandboxedFetchMock); + }, + }); + } + return originalFetchMock[name]; + }, + }); + + // spy on the fetch handler so we can use all the + // jest function assertions on it + const spy = jest.fn(); + const originalFetchHandler = jestifiedInstance.fetchHandler.bind( + jestifiedInstance + ); + + jestifiedInstance.fetchHandler = function (...args) { + const result = originalFetchHandler(...args); + spy.mockReturnValueOnce(result); + spy.apply(this, args); + return result; + }; + + // make sure all the jest expectation helpers can find what they need on fetchMock.mock + Object.assign(jestifiedInstance.mock, spy.mock); + + ['_isMockFunction', 'mockName', 'getMockName'].forEach((prop) => { + jestifiedInstance[prop] = spy[prop]; + }); + + jestifiedInstance.mockClear = () => { + spy.mockClear(); + jestifiedInstance.resetHistory(); + Object.assign(jestifiedInstance.mock, spy.mock); + }; + jestifiedInstance.mockReset = () => { + spy.mockReset(); + jestifiedInstance.reset(); + Object.assign(jestifiedInstance.mock, spy.mock); + }; + jestifiedInstance.mockRestore = () => { + throw new Error( + "mockRestore not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }; + jestifiedInstance.mockImplementation = () => { + throw new Error( + "mockImplementation not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }; + jestifiedInstance.mockImplementationOnce = () => { + throw new Error( + "mockImplementationOnce not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }; + jestifiedInstance.mockName = () => { + throw new Error( + "mockName not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }; + jestifiedInstance.mockReturnThis = () => { + throw new Error( + "mockReturnThis not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }; + jestifiedInstance.mockReturnValue = () => { + throw new Error( + "mockReturnValue not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }; + jestifiedInstance.mockReturnValueOnce = () => { + throw new Error( + "mockReturnValueOnce not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }; + jestifiedInstance.mockResolvedValue = () => { + throw new Error( + "mockResolvedValue not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }; + jestifiedInstance.mockResolvedValueOnce = () => { + throw new Error( + "mockResolvedValueOnce not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }; + jestifiedInstance.mockRejectedValue = () => { + throw new Error( + "mockRejectedValue not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }; + jestifiedInstance.mockRejectedValueOnce = () => { + throw new Error( + "mockRejectedValueOnce not supported on fetch-mock. Use fetch-mock's methods to manage mock responses" + ); + }; + + // make sure that the mock object that has properties updated + // by the jest spy is the one that is exposed on fetch + spy.mock = jestifiedInstance.mock; + + // Return this monster! + return jestifiedInstance; +}; + +module.exports = (fetchMock) => jestify(fetchMock); diff --git a/packages/vitest/package.json b/packages/vitest/package.json new file mode 100644 index 00000000..b0e91bf0 --- /dev/null +++ b/packages/vitest/package.json @@ -0,0 +1,22 @@ +{ + "name": "@fetch-mock/vitest", + "version": "0.0.0", + "description": "Vitest wrapper for fetch-mock", + "main": "./dist/commonjs.js", + "module": "./src/index.js", + "exports": { + "types": "./types/index.d.ts", + "browser": "./src/index.js", + "import": "./src/index.js", + "require": "./dist/commonjs.js" + }, + "types": "./types/index.d.ts", + "scripts": { + "build": "rollup -c" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@fetch-mock/core": "^0.4.8" + } +} diff --git a/packages/vitest/rollup.config.mjs b/packages/vitest/rollup.config.mjs new file mode 100644 index 00000000..d2a2e998 --- /dev/null +++ b/packages/vitest/rollup.config.mjs @@ -0,0 +1,2 @@ +import rollupConfig from '../../shared-rollup.config.js'; +export default rollupConfig; diff --git a/packages/vitest/src/index.js b/packages/vitest/src/index.js new file mode 100644 index 00000000..c622696e --- /dev/null +++ b/packages/vitest/src/index.js @@ -0,0 +1,44 @@ +import { vi } from 'vitest'; +import { FetchMock, defaultConfig } from '@fetch-mock/core'; +import './vitest-extensions'; + +class FetchMockVitest extends FetchMock { + mockClear () { + this.callHistory.clear(); + return this + } + mockReset ({includeSticky=false, includeFallback=true} = {}) { + this.router.removeRoutes({includeSticky, includeFallback}); + return this.mockClear(); + } + mockRestore(options) { + this.unmockGlobal(); + return this.mockReset(options) + } +} + +const fetchMockVitest = new FetchMockVitest({ + ...defaultConfig, +}) + + +export function hookIntoVitestMockResetMethods() { + const { clearAllMocks, resetAllMocks, restoreAllMocks } = vi; + + vi.clearAllMocks = () => { + clearAllMocks.apply(vi); + fetchMockVitest.mockClear(); + } + + vi.resetAllMocks = (options) => { + resetAllMocks.apply(vi); + fetchMockVitest.mockReset(options); + } + + vi.restoreAllMocks = (options) => { + restoreAllMocks.apply(vi); + fetchMockVitest.mockRestore(options); + } +} + +export default fetchMockVitest; diff --git a/packages/vitest/src/package.json b/packages/vitest/src/package.json new file mode 100644 index 00000000..6990891f --- /dev/null +++ b/packages/vitest/src/package.json @@ -0,0 +1 @@ +{"type": "module"} diff --git a/packages/vitest/src/vitest-extensions.js b/packages/vitest/src/vitest-extensions.js new file mode 100644 index 00000000..fbd55a00 --- /dev/null +++ b/packages/vitest/src/vitest-extensions.js @@ -0,0 +1,100 @@ +import {expect} from "vitest"; + +const methodlessExtensions = { + toHaveFetched: ({fetchMock}, matcher, options) => { + if (fetchMock.called(matcher, options)) { + return { pass: true }; + } + return { + pass: false, + message: () => `fetch should have been called with ${matcher} and ${JSON.stringify(options)}`, + }; + }, + toHaveLastFetched: ({fetchMock}, matcher, options) => { + const allCalls = fetchMock.callHistory.calls(); + if (!allCalls.length) { + return { + pass: false, + message: () => `No calls made to fetch`, + }; + } + const lastCall = [...allCalls].pop(); + const lastMatchingCall = [...fetchMock.callHistory.calls(matcher, options)].pop(); + if (lastCall === lastMatchingCall) { + return { pass: true }; + } + return { + pass: false, + message: () => + `Last call to fetch should have matched ${matcher} and ${JSON.stringify(options)} but was ${JSON.stringify(lastCall)}`, + }; + }, + + toHaveNthFetched: ({fetchMock}, n, matcher, options) => { + const nthCall = fetchMock.callHistory.calls()[n - 1]; + const matchingCalls = fetchMock.callHistory.calls(matcher, options); + if (matchingCalls.some((call) => call === nthCall)) { + return { pass: true }; + } + return { + pass: false, + message: () => + `${n}th call to fetch should have matched ${matcher} and ${JSON.stringify(options)} but was ${JSON.stringify(nthCall)}`, + }; + }, + + toHaveFetchedTimes: ({fetchMock}, times, matcher, options) => { + const calls = fetchMock.callHistory.calls(matcher, options); + if (calls.length === times) { + return { pass: true }; + } + return { + pass: false, + message: () => + `fetch should have made ${times} calls matching ${matcher} and ${JSON.stringify(options)}, but it only made ${calls.length}`, + }; + }, +}; + +expect.extend(methodlessExtensions); +expect.extend({ + toBeDone: ({fetchMock}, filter) => { + const done = fetchMock.callHistory.done(filter); + if (done) { + return { pass: true }; + } + return { + pass: false, + message: () => + `fetch has not been called the expected number of times ${ + filter ? `for ${filter}` : 'in total' + }`, + }; + }, +}); + +[ + 'Got:get', + 'Posted:post', + 'Put:put', + 'Deleted:delete', + 'FetchedHead:head', + 'Patched:patch', +].forEach((verbs) => { + const [humanVerb, method] = verbs.split(':'); + + const extensions = Object.entries(methodlessExtensions) + .map(([name, func]) => { + return [ + (name = name.replace('Fetched', humanVerb)), + (...args) => { + const opts = args[func.length - 1] || {}; + args[func.length - 1] = { ...opts, method }; + return func(...args); + }, + ]; + }) + .reduce((obj, [name, func]) => ({ ...obj, [name]: func }), {}); + + expect.extend(extensions); +});