From 45eadd70bdf72e20d118f35b92b4f6fd5a7dbb9f Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Wed, 24 Jul 2024 23:32:26 +0100 Subject: [PATCH 1/9] feat: started work on @fetch-mock/standalone --- packages/core/src/FetchMock.js | 2 +- packages/core/src/index.js | 3 +- packages/standalone/README.md | 31 ++++++++++++++ packages/standalone/fragments.js | 69 -------------------------------- packages/standalone/index.mjs | 36 +++++++++++++++++ packages/standalone/types.ts | 30 -------------- 6 files changed, 70 insertions(+), 101 deletions(-) create mode 100644 packages/standalone/README.md delete mode 100644 packages/standalone/fragments.js create mode 100644 packages/standalone/index.mjs delete mode 100644 packages/standalone/types.ts diff --git a/packages/core/src/FetchMock.js b/packages/core/src/FetchMock.js index 9bedfef1a..42356c217 100644 --- a/packages/core/src/FetchMock.js +++ b/packages/core/src/FetchMock.js @@ -96,7 +96,7 @@ const defineGreedyShorthand = (shorthandOptions) => { }; }; -class FetchMock { +export class FetchMock { /** * * @param {FetchMockConfig} config diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 75a259004..f6712b615 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,2 +1,3 @@ -import fetchMock from './FetchMock.js'; +import fetchMock, {FetchMock} from './FetchMock.js'; export default fetchMock; +export const FetchMock; diff --git a/packages/standalone/README.md b/packages/standalone/README.md new file mode 100644 index 000000000..d2d8ba99b --- /dev/null +++ b/packages/standalone/README.md @@ -0,0 +1,31 @@ +# @fetch-mock/standalone + +Wrapper around @fetch-mock/core that implements mocking of global fetch, including spying on and falling through to the native fetch implementation. + +In addition to the @fetch-mock/core API its methods are: + +## mockGlobal() + +Replaces `fetch` with `fm.fetchHandler` + +## restoreGlobal() + +Restores `fetch` to its original state + +## spyGlobal() + +Replaces `fetch` with `fm.fetchHandler`, but falls back to the network for any unmatched calls + +## spyLocal(fetchImplementation) + + + + +mock() +@fetch-mock/core does not implement any functionality for replacing global fetch or a local fetch implementation (such as node-fetch) with a mock implementation. + +.spy()/fallbackToNetwork +As @fetch-mock/core does not do anything to replace the native fetch implementation, these features - which pass through the fetch-mock implementation and go straight to the native implementation - are also concersn that will be added to wrappers. + +restore()/reset() +Libraries such as Jest or Vitest have their own APIs for resetting mocks, so @fetch-mock/core deliberately only contains low level APIs for managing routes and call history. These will be wrapped in ways that are idiomatic for different test frameworks. diff --git a/packages/standalone/fragments.js b/packages/standalone/fragments.js deleted file mode 100644 index 3e4196ffb..000000000 --- a/packages/standalone/fragments.js +++ /dev/null @@ -1,69 +0,0 @@ -FetchHandler.getNativeFetch = function () { - const func = this.realFetch || (this.isSandbox && this.config.fetch); - if (!func) { - throw new Error( - 'fetch-mock: Falling back to network only available on global fetch-mock, or by setting config.fetch on sandboxed fetch-mock', - ); - } - return func; -}; - - -FetchMock.resetBehavior = function (options = {}) { - const removeRoutes = getRouteRemover(options); - - this.routes = removeRoutes(this.routes); - this._uncompiledRoutes = removeRoutes(this._uncompiledRoutes); - - if (this.realFetch && !this.routes.length) { - globalThis.fetch = this.realFetch; - this.realFetch = undefined; - } - - this.fallbackResponse = undefined; - return this; -}; - -FetchMock.resetHistory = function () { - this._calls = []; - this._holdingPromises = []; - this.routes.forEach((route) => route.reset && route.reset()); - return this; -}; - -FetchMock.restore = FetchMock.reset = function (options) { - this.resetBehavior(options); - this.resetHistory(); - return this; -}; - -FetchMock._mock = function () { - if (!this.isSandbox) { - // Do this here rather than in the constructor to ensure it's scoped to the test - this.realFetch = this.realFetch || globalThis.fetch; - globalThis.fetch = this.fetchHandler; - } - return this; -}; - -if (this.getOption('fallbackToNetwork') === 'always') { - return { - route: { response: this.getNativeFetch(), responseIsFetch: true }, - // BUG - this callLog never used to get sent. Discovered the bug - // but can't fix outside a major release as it will potentially - // cause too much disruption - // - // callLog, - }; -} - - if (!this.getOption('fallbackToNetwork')) { - throw new Error( - `fetch-mock: No fallback response defined for ${(options && options.method) || 'GET' - } to ${url}`, - ); - } - return { - route: { response: this.getNativeFetch(), responseIsFetch: true }, - callLog, - }; \ No newline at end of file diff --git a/packages/standalone/index.mjs b/packages/standalone/index.mjs new file mode 100644 index 000000000..be52ce61a --- /dev/null +++ b/packages/standalone/index.mjs @@ -0,0 +1,36 @@ +import {FetchMock} from '@fetch-mock/core'; + +// TODO +// Maybe this IS part of fetch-mock +// fetch mock exports fetchMock (instanceof FetchMockStandalone), and FetchMock, +// which is extended by the other wrappers +class FetchMockStandalone extends FetchMock { + mockGlobal() { + this.#originalFetch = globalThis.fetch; + globalThis.fetch = this.fetchHandler.bind(this); + return this + } + + restoreGlobal() { + globalThis.fetch = this.#originalFetch + return this + } + + spyGlobal() { + this.#originalFetch = globalThis.fetch; + globalThis.fetch = this.fetchHandler.bind(this); + + this.catch(({arguments}) => this.#originalFetch(...arguments)) + return this + } + + spyLocal(fetchImplementation) { + this.#originalFetch = fetchImplementation; + this.catch(({arguments}) => this.#originalFetch(...arguments)) + return this + } + + createInstance() { + return new FetchMockStandalone({ ...this.config }, this.router); + } +} diff --git a/packages/standalone/types.ts b/packages/standalone/types.ts deleted file mode 100644 index f57bedf54..000000000 --- a/packages/standalone/types.ts +++ /dev/null @@ -1,30 +0,0 @@ -// /** -// * Chainable method that records the call history of unmatched calls, -// * but instead of responding with a stubbed response, the request is -// * passed through to native fetch() and is allowed to communicate -// * over the network. Similar to catch(). -// */ -// spy(response?: MockResponse | MockResponseFunction): this; - -// /** -// * Restores fetch() to its unstubbed state and clears all data recorded -// * for its calls. reset() is an alias for restore(). -// */ -// restore(): this; - -// /** -// * Restores fetch() to its unstubbed state and clears all data recorded -// * for its calls. reset() is an alias for restore(). -// */ -// reset(): this; - -// /** -// * Clears all data recorded for fetch()’s calls. It will not restore -// * fetch to its default implementation. -// */ -// resetHistory(): this; - -// /** -// * Removes mocking behaviour without resetting call history. -// */ -// resetBehavior(): this; \ No newline at end of file From bc4e4ef2a76acf24c57bfa177e85907c24774bae Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Thu, 25 Jul 2024 13:30:15 +0100 Subject: [PATCH 2/9] feat: moved standalone methods into fetchmock core --- packages/core/src/CallHistory.js | 2 +- packages/core/src/FetchMock.js | 56 +++++++++++++++++++++++---- packages/core/src/RequestUtils.js | 4 +- packages/core/src/index.js | 4 +- packages/core/types/CallHistory.d.ts | 2 +- packages/core/types/FetchMock.d.ts | 58 ++++++++++++++++------------ packages/core/types/index.d.ts | 2 + 7 files changed, 90 insertions(+), 38 deletions(-) diff --git a/packages/core/src/CallHistory.js b/packages/core/src/CallHistory.js index c5136defd..873d41592 100644 --- a/packages/core/src/CallHistory.js +++ b/packages/core/src/CallHistory.js @@ -11,7 +11,7 @@ import Router from './Router.js'; /** * @typedef CallLog - * @property {any[]} arguments + * @property {any[]} args * @property {string} url * @property {NormalizedRequestOptions} options * @property {Request} [request] diff --git a/packages/core/src/FetchMock.js b/packages/core/src/FetchMock.js index 42356c217..5547d34b7 100644 --- a/packages/core/src/FetchMock.js +++ b/packages/core/src/FetchMock.js @@ -119,7 +119,7 @@ export class FetchMock { } /** * - * @param {string | Request} requestInput + * @param {string | URL | Request} requestInput * @param {RequestInit} [requestInit] * @this {FetchMock} * @returns {Promise} @@ -162,7 +162,6 @@ export class FetchMock { * @param {RouteResponse} [response] * @param {UserRouteConfig | string} [options] * @this {FetchMock} - * @returns {FetchMock} */ route(matcher, response, options) { this.router.addRoute(matcher, response, options); @@ -172,7 +171,6 @@ export class FetchMock { * * @param {RouteResponse} [response] * @this {FetchMock} - * @returns {FetchMock} */ catch(response) { this.router.setFallback(response); @@ -193,15 +191,13 @@ export class FetchMock { * @param {boolean} [options.includeSticky] * @param {boolean} [options.includeFallback] * @this {FetchMock} - * @returns {FetchMock} */ removeRoutes(options) { this.router.removeRoutes(options); return this; } /** - * - * @returns {FetchMock} + * @this {FetchMock} */ clearHistory() { this.callHistory.clear(); @@ -225,6 +221,52 @@ export class FetchMock { patchOnce = defineShorthand({ method: 'patch', repeat: 1 }); } -const fetchMock = new FetchMock({ ...defaultConfig }).createInstance(); +class FetchMockStandalone extends FetchMock { + /** @type {typeof fetch} */ + #originalFetch = null; + /** + * @this {FetchMockStandalone} + */ + mockGlobal() { + this.#originalFetch = globalThis.fetch; + globalThis.fetch = this.fetchHandler.bind(this); + return this; + } + /** + * @this {FetchMockStandalone} + */ + restoreGlobal() { + globalThis.fetch = this.#originalFetch; + return this; + } + /** + * @this {FetchMockStandalone} + */ + spyGlobal() { + this.#originalFetch = globalThis.fetch; + globalThis.fetch = this.fetchHandler.bind(this); + // @ts-ignore + this.catch(({ args }) => this.#originalFetch(...args)); + return this; + } + /** + * @param {typeof fetch} fetchImplementation + * @this {FetchMockStandalone} + */ + spyLocal(fetchImplementation) { + this.#originalFetch = fetchImplementation; + // @ts-ignore + this.catch(({ args }) => this.#originalFetch(...args)); + return this; + } + + createInstance() { + return new FetchMockStandalone({ ...this.config }, this.router); + } +} + +const fetchMock = new FetchMockStandalone({ + ...defaultConfig, +}).createInstance(); export default fetchMock; diff --git a/packages/core/src/RequestUtils.js b/packages/core/src/RequestUtils.js index e4a5b9472..5fc18eb33 100644 --- a/packages/core/src/RequestUtils.js +++ b/packages/core/src/RequestUtils.js @@ -45,7 +45,7 @@ export function createCallLogFromUrlAndOptions(url, options) { // @ts-ignore - jsdoc doesn't distinguish between string and String, but typechecker complains url = normalizeUrl(url); return { - arguments: [url, options], + args: [url, options], url, queryParams: new URLSearchParams(getQuery(url)), options: options || {}, @@ -85,7 +85,7 @@ export async function createCallLogFromRequest(request, options) { } const url = normalizeUrl(request.url); const callLog = { - arguments: [request, options], + args: [request, options], url, queryParams: new URLSearchParams(getQuery(url)), options: Object.assign(derivedOptions, options || {}), diff --git a/packages/core/src/index.js b/packages/core/src/index.js index f6712b615..39686b202 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,3 +1,3 @@ -import fetchMock, {FetchMock} from './FetchMock.js'; +import fetchMock, { FetchMock } from './FetchMock.js'; export default fetchMock; -export const FetchMock; +export { FetchMock }; diff --git a/packages/core/types/CallHistory.d.ts b/packages/core/types/CallHistory.d.ts index f50b9a363..902e04813 100644 --- a/packages/core/types/CallHistory.d.ts +++ b/packages/core/types/CallHistory.d.ts @@ -5,7 +5,7 @@ export type NormalizedRequestOptions = import("./RequestUtils.js").NormalizedReq export type RouteMatcher = import("./Matchers.js").RouteMatcher; export type FetchMockConfig = import("./FetchMock.js").FetchMockConfig; export type CallLog = { - arguments: any[]; + args: any[]; url: string; options: NormalizedRequestOptions; request?: Request; diff --git a/packages/core/types/FetchMock.d.ts b/packages/core/types/FetchMock.d.ts index 152a6b21b..eea8ffe7f 100644 --- a/packages/core/types/FetchMock.d.ts +++ b/packages/core/types/FetchMock.d.ts @@ -1,31 +1,10 @@ -export default fetchMock; -export type RouteMatcher = import("./Router.js").RouteMatcher; -export type RouteName = import("./Route.js").RouteName; -export type UserRouteConfig = import("./Route.js").UserRouteConfig; -export type RouteResponse = import("./Router.js").RouteResponse; -export type MatcherDefinition = import("./Matchers.js").MatcherDefinition; -export type CallLog = import("./CallHistory.js").CallLog; -export type RouteResponseFunction = import("./Route.js").RouteResponseFunction; -export type FetchMockGlobalConfig = { - sendAsJson?: boolean; - includeContentLength?: boolean; - matchPartialBody?: boolean; -}; -export type FetchImplementations = { - fetch?: (arg0: string | Request, arg1: RequestInit) => Promise; - Headers?: typeof Headers; - Request?: typeof Request; - Response?: typeof Response; -}; -export type FetchMockConfig = FetchMockGlobalConfig & FetchImplementations; -declare const fetchMock: FetchMock; -declare class FetchMock { +export class FetchMock { constructor(config: FetchMockConfig, router?: Router); config: FetchMockConfig; router: Router; callHistory: CallHistory; createInstance(): FetchMock; - fetchHandler(this: FetchMock, requestInput: string | Request, requestInit?: RequestInit): Promise; + fetchHandler(this: FetchMock, requestInput: string | URL | Request, requestInit?: RequestInit): Promise; route(matcher: UserRouteConfig): FetchMock; route(matcher: RouteMatcher, response: RouteResponse, options?: UserRouteConfig | string): FetchMock; catch(this: FetchMock, response?: RouteResponse): FetchMock; @@ -34,8 +13,8 @@ declare class FetchMock { names?: string[]; includeSticky?: boolean; includeFallback?: boolean; - }): FetchMock; - clearHistory(): FetchMock; + }): this; + clearHistory(this: FetchMock): FetchMock; sticky: { (this: FetchMock, matcher: UserRouteConfig): FetchMock; (this: FetchMock, matcher: RouteMatcher, response: RouteResponse, options?: UserRouteConfig | string): FetchMock; @@ -95,5 +74,34 @@ declare class FetchMock { (this: FetchMock, matcher: RouteMatcher, response: RouteResponse, options?: UserRouteConfig | string): FetchMock; }; } +export default fetchMock; +export type RouteMatcher = import("./Router.js").RouteMatcher; +export type RouteName = import("./Route.js").RouteName; +export type UserRouteConfig = import("./Route.js").UserRouteConfig; +export type RouteResponse = import("./Router.js").RouteResponse; +export type MatcherDefinition = import("./Matchers.js").MatcherDefinition; +export type CallLog = import("./CallHistory.js").CallLog; +export type RouteResponseFunction = import("./Route.js").RouteResponseFunction; +export type FetchMockGlobalConfig = { + sendAsJson?: boolean; + includeContentLength?: boolean; + matchPartialBody?: boolean; +}; +export type FetchImplementations = { + fetch?: (arg0: string | Request, arg1: RequestInit) => Promise; + Headers?: typeof Headers; + Request?: typeof Request; + Response?: typeof Response; +}; +export type FetchMockConfig = FetchMockGlobalConfig & FetchImplementations; import Router from './Router.js'; import CallHistory from './CallHistory.js'; +declare const fetchMock: FetchMockStandalone; +declare class FetchMockStandalone extends FetchMock { + mockGlobal(this: FetchMockStandalone): FetchMockStandalone; + restoreGlobal(this: FetchMockStandalone): FetchMockStandalone; + spyGlobal(this: FetchMockStandalone): FetchMockStandalone; + spyLocal(this: FetchMockStandalone, fetchImplementation: typeof fetch): FetchMockStandalone; + createInstance(): FetchMockStandalone; + #private; +} diff --git a/packages/core/types/index.d.ts b/packages/core/types/index.d.ts index 1ae742766..79c86114d 100644 --- a/packages/core/types/index.d.ts +++ b/packages/core/types/index.d.ts @@ -1,2 +1,4 @@ export default fetchMock; +export { FetchMock }; import fetchMock from './FetchMock.js'; +import { FetchMock } from './FetchMock.js'; From 97b8dd5d8a50810c1563c5ed3524edaf2dae9f91 Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Thu, 25 Jul 2024 13:39:17 +0100 Subject: [PATCH 3/9] docs: document the mocking methods --- .../@fetch-mock/core/mocking-and-spying.md | 29 +++++++++++++++ packages/standalone/README.md | 31 ---------------- packages/standalone/index.mjs | 36 ------------------- 3 files changed, 29 insertions(+), 67 deletions(-) create mode 100644 docs/docs/@fetch-mock/core/mocking-and-spying.md delete mode 100644 packages/standalone/README.md delete mode 100644 packages/standalone/index.mjs diff --git a/docs/docs/@fetch-mock/core/mocking-and-spying.md b/docs/docs/@fetch-mock/core/mocking-and-spying.md new file mode 100644 index 000000000..42e8ed253 --- /dev/null +++ b/docs/docs/@fetch-mock/core/mocking-and-spying.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 3 +--- + +# Mocking and spying + +These methods allow mocking or spying on the `fetch` implementation used by your application. + +Note that these methods are only implemented in `@fetch-mock/core` and are not avilable when using `@fetch-mock/jest`, `@fetch-mock/vitest` etc.... Those libraries provide ways to mock `fetch` that are more idiomatic to their own ecosystem. + +Wrapper around @fetch-mock/core that implements mocking of global fetch, including spying on and falling through to the native fetch implementation. + +In addition to the @fetch-mock/core API its methods are: + +## mockGlobal() + +Replaces `fetch` with `fm.fetchHandler` + +## restoreGlobal() + +Restores `fetch` to its original state + +## spyGlobal() + +Replaces `fetch` with `fm.fetchHandler`, but falls back to the network for any unmatched calls + +## 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. diff --git a/packages/standalone/README.md b/packages/standalone/README.md deleted file mode 100644 index d2d8ba99b..000000000 --- a/packages/standalone/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# @fetch-mock/standalone - -Wrapper around @fetch-mock/core that implements mocking of global fetch, including spying on and falling through to the native fetch implementation. - -In addition to the @fetch-mock/core API its methods are: - -## mockGlobal() - -Replaces `fetch` with `fm.fetchHandler` - -## restoreGlobal() - -Restores `fetch` to its original state - -## spyGlobal() - -Replaces `fetch` with `fm.fetchHandler`, but falls back to the network for any unmatched calls - -## spyLocal(fetchImplementation) - - - - -mock() -@fetch-mock/core does not implement any functionality for replacing global fetch or a local fetch implementation (such as node-fetch) with a mock implementation. - -.spy()/fallbackToNetwork -As @fetch-mock/core does not do anything to replace the native fetch implementation, these features - which pass through the fetch-mock implementation and go straight to the native implementation - are also concersn that will be added to wrappers. - -restore()/reset() -Libraries such as Jest or Vitest have their own APIs for resetting mocks, so @fetch-mock/core deliberately only contains low level APIs for managing routes and call history. These will be wrapped in ways that are idiomatic for different test frameworks. diff --git a/packages/standalone/index.mjs b/packages/standalone/index.mjs deleted file mode 100644 index be52ce61a..000000000 --- a/packages/standalone/index.mjs +++ /dev/null @@ -1,36 +0,0 @@ -import {FetchMock} from '@fetch-mock/core'; - -// TODO -// Maybe this IS part of fetch-mock -// fetch mock exports fetchMock (instanceof FetchMockStandalone), and FetchMock, -// which is extended by the other wrappers -class FetchMockStandalone extends FetchMock { - mockGlobal() { - this.#originalFetch = globalThis.fetch; - globalThis.fetch = this.fetchHandler.bind(this); - return this - } - - restoreGlobal() { - globalThis.fetch = this.#originalFetch - return this - } - - spyGlobal() { - this.#originalFetch = globalThis.fetch; - globalThis.fetch = this.fetchHandler.bind(this); - - this.catch(({arguments}) => this.#originalFetch(...arguments)) - return this - } - - spyLocal(fetchImplementation) { - this.#originalFetch = fetchImplementation; - this.catch(({arguments}) => this.#originalFetch(...arguments)) - return this - } - - createInstance() { - return new FetchMockStandalone({ ...this.config }, this.router); - } -} From bc4457d25eb1b4cd460fa9c920f6207a93e0e0ec Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Thu, 25 Jul 2024 13:42:09 +0100 Subject: [PATCH 4/9] chore: remove placeholder packages --- package.json | 2 +- .../__tests__/FetchMock/mock-and-spy.test.js | 640 ++++++++++++++++++ packages/standalone/fallbackToNetwork.test.js | 214 ------ packages/standalone/global-fetch.test.js | 136 ---- .../standalone/set-up-and-tear-down.test.js | 198 ------ packages/standalone/spy.test.js | 89 --- packages/wip/generated-types/CallHistory.d.ts | 11 - .../wip/generated-types/FetchHandler.d.ts | 17 - .../wip/generated-types/FetchMockWrapper.d.ts | 2 - packages/wip/generated-types/Matchers.d.ts | 16 - .../wip/generated-types/RequestUtils.d.ts | 27 - .../wip/generated-types/ResponseBuilder.d.ts | 2 - packages/wip/generated-types/Route.d.ts | 44 -- packages/wip/generated-types/Router.d.ts | 3 - .../wip/generated-types/StatusTextMap.d.ts | 7 - .../__tests__/CallHistory.test.d.ts | 1 - .../__tests__/FetchHandler.test.d.ts | 1 - .../__tests__/FetchMockWrapper.test.d.ts | 1 - .../__tests__/Matchers.test.d.ts | 1 - .../__tests__/ResponseBuilder.test.d.ts | 1 - .../__tests__/Router/Router.test.d.ts | 1 - .../__tests__/Router/body-matching.test.d.ts | 1 - .../__tests__/Router/edge-cases.test.d.ts | 1 - .../Router/function-matching.test.d.ts | 1 - .../Router/header-matching.test.d.ts | 1 - .../Router/matchPartialBody.test.d.ts | 1 - .../__tests__/Router/matcher-object.test.d.ts | 1 - .../Router/method-matching.test.d.ts | 1 - .../Router/multiple-routes.test.d.ts | 1 - .../__tests__/Router/naming-routes.test.d.ts | 1 - .../Router/path-parameter-matching.test.d.ts | 1 - .../Router/query-string-matching.test.d.ts | 1 - .../__tests__/Router/sticky-routes.test.d.ts | 1 - .../Router/unmatched-calls.test.d.ts | 1 - .../__tests__/Router/url-matching.test.d.ts | 1 - packages/wip/old-types/CallHistory.d.ts | 159 ----- packages/wip/old-types/FetchMockWrapper.d.ts | 84 --- packages/wip/old-types/RequestUtils.d.ts | 27 - packages/wip/old-types/ResponseBuilder.d.ts | 54 -- packages/wip/old-types/StatusTextMap.d.ts | 7 - packages/wip/old-types/index.d.ts | 16 - 41 files changed, 641 insertions(+), 1134 deletions(-) create mode 100644 packages/core/src/__tests__/FetchMock/mock-and-spy.test.js delete mode 100644 packages/standalone/fallbackToNetwork.test.js delete mode 100644 packages/standalone/global-fetch.test.js delete mode 100644 packages/standalone/set-up-and-tear-down.test.js delete mode 100644 packages/standalone/spy.test.js delete mode 100644 packages/wip/generated-types/CallHistory.d.ts delete mode 100644 packages/wip/generated-types/FetchHandler.d.ts delete mode 100644 packages/wip/generated-types/FetchMockWrapper.d.ts delete mode 100644 packages/wip/generated-types/Matchers.d.ts delete mode 100644 packages/wip/generated-types/RequestUtils.d.ts delete mode 100644 packages/wip/generated-types/ResponseBuilder.d.ts delete mode 100644 packages/wip/generated-types/Route.d.ts delete mode 100644 packages/wip/generated-types/Router.d.ts delete mode 100644 packages/wip/generated-types/StatusTextMap.d.ts delete mode 100644 packages/wip/generated-types/__tests__/CallHistory.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/FetchHandler.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/FetchMockWrapper.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Matchers.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/ResponseBuilder.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Router/Router.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Router/body-matching.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Router/edge-cases.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Router/function-matching.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Router/header-matching.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Router/matchPartialBody.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Router/matcher-object.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Router/method-matching.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Router/multiple-routes.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Router/naming-routes.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Router/path-parameter-matching.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Router/query-string-matching.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Router/sticky-routes.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Router/unmatched-calls.test.d.ts delete mode 100644 packages/wip/generated-types/__tests__/Router/url-matching.test.d.ts delete mode 100644 packages/wip/old-types/CallHistory.d.ts delete mode 100644 packages/wip/old-types/FetchMockWrapper.d.ts delete mode 100644 packages/wip/old-types/RequestUtils.d.ts delete mode 100644 packages/wip/old-types/ResponseBuilder.d.ts delete mode 100644 packages/wip/old-types/StatusTextMap.d.ts delete mode 100644 packages/wip/old-types/index.d.ts diff --git a/package.json b/package.json index 73a964b7c..6f41fc3ff 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "types:check": "tsc --project ./jsconfig.json && echo 'types check done'", "types:lint": "dtslint --expectOnly packages/fetch-mock/types", "prepare": "husky || echo \"husky not available\"", - "build": "npm run build -w=packages/fetch-mock -w=packages/core", + "build": "npm run build -w=packages/*", "docs": "npm run start -w docs", "test:ci": "vitest .", "test:legacy": "vitest ./packages/fetch-mock/test/specs", diff --git a/packages/core/src/__tests__/FetchMock/mock-and-spy.test.js b/packages/core/src/__tests__/FetchMock/mock-and-spy.test.js new file mode 100644 index 000000000..636190eee --- /dev/null +++ b/packages/core/src/__tests__/FetchMock/mock-and-spy.test.js @@ -0,0 +1,640 @@ +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', () => +// new Promise((res) => { +// const readable = new Readable(); +// const write = vi.fn().mockImplementation((chunk, enc, cb) => { +// cb(); +// }); +// const writable = new Writable({ +// write, +// }); +// readable.push('response string'); +// readable.push(null); + +// fetchMock.route(/a/, readable, { sendAsJson: false }); +// fetchMock.fetchHandler('http://a.com').then((res) => { +// res.body.pipe(writable); +// }); + +// writable.on('finish', () => { +// expect(write.args[0][0].toString('utf8')).to.equal('response string'); +// res(); +// }); +// })); + +// // See https://github.com/wheresrhys/fetch-mock/issues/575 +// it('can respond with large bodies from the interweb', async () => { +// const fm = fetchMock.sandbox(); +// fm.config.fallbackToNetwork = true; +// fm.route(); +// // this is an adequate test because the response hangs if the +// // bug referenced above creeps back in +// await fm +// .fetchHandler('http://www.wheresrhys.co.uk/assets/img/chaffinch.jpg') +// .then((res) => res.blob()); +// }); + + + + +// describe.skip('client-side only tests', () => { +// it('not throw when passing unmatched calls through to native fetch', () => { +// fetchMock.config.fallbackToNetwork = true; +// fetchMock.route(); +// expect(() => fetch('http://a.com')).not.to.throw(); +// fetchMock.config.fallbackToNetwork = false; +// }); + +// // this is because we read the body once when normalising the request and +// // want to make sure fetch can still use the sullied request +// it.skip('can send a body on a Request instance when spying ', async () => { +// fetchMock.spy(); +// const req = new fetchMock.config.Request('http://example.com', { +// method: 'post', +// body: JSON.stringify({ prop: 'val' }), +// }); +// try { +// await fetch(req); +// } catch (err) { +// console.log(err); +// expect.unreachable('Fetch should not throw or reject'); +// } +// }); + +// // 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(); +// }); +// } + + + +// it.skip('should cope when there is no global fetch defined', () => { +// const originalFetch = globalThis.fetch; +// delete globalThis.fetch; +// const originalRealFetch = fetchMock.realFetch; +// delete fetchMock.realFetch; +// fetchMock.route('*', 200); +// expect(() => { +// fetch('http://a.com'); +// }).not.to.throw(); + +// expect(() => { +// fetchMock.calls(); +// }).not.to.throw(); +// fetchMock.restore(); +// fetchMock.realFetch = originalRealFetch; +// globalThis.fetch = originalFetch; +// }); + +// if (globalThis.navigator?.serviceWorker) { +// it('should work within a service worker', async () => { +// const registration = +// await globalThis.navigator.serviceWorker.register('__sw.js'); +// await new Promise((resolve, reject) => { +// if (registration.installing) { +// registration.installing.onstatechange = function () { +// if (this.state === 'activated') { +// resolve(); +// } +// }; +// } else { +// reject('No idea what happened'); +// } +// }); + +// await registration.unregister(); +// }); +// } + +// }); + + + + + + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { fetchMock } = testGlobals; + +describe('use with global fetch', () => { + let originalFetch; + + const expectToBeStubbed = (yes = true) => { + expect(globalThis.fetch).toEqual( + yes ? fetchMock.fetchHandler : originalFetch, + ); + expect(globalThis.fetch).not.toEqual( + yes ? originalFetch : fetchMock.fetchHandler, + ); + }; + + beforeEach(() => { + originalFetch = globalThis.fetch = vi.fn().mockResolvedValue(); + }); + afterEach(fetchMock.restore); + + it('replaces global fetch when mock called', () => { + fetchMock.mock('*', 200); + expectToBeStubbed(); + }); + + it('replaces global fetch when catch called', () => { + fetchMock.catch(200); + expectToBeStubbed(); + }); + + it('replaces global fetch when spy called', () => { + fetchMock.spy(); + expectToBeStubbed(); + }); + + it('restores global fetch after a mock', () => { + fetchMock.mock('*', 200).restore(); + expectToBeStubbed(false); + }); + + it('restores global fetch after a complex mock', () => { + fetchMock.mock('a', 200).mock('b', 200).spy().catch(404).restore(); + expectToBeStubbed(false); + }); + + it('not call default fetch when in mocked mode', async () => { + fetchMock.mock('*', 200); + + await globalThis.fetch('http://a.com'); + expect(originalFetch).not.toHaveBeenCalled(); + }); +}); +let originalFetch; + +beforeAll(() => { + originalFetch = globalThis.fetch = vi.fn().mockResolvedValue('dummy'); +}); + +it('return function', () => { + const sbx = fetchMock.sandbox(); + expect(typeof sbx).toEqual('function'); +}); + + + +it("don't interfere with global fetch", () => { + const sbx = fetchMock.sandbox().route('http://a.com', 200); + + expect(globalThis.fetch).toEqual(originalFetch); + expect(globalThis.fetch).not.toEqual(sbx); +}); + +it("don't interfere with global fetch-mock", async () => { + const sbx = fetchMock.sandbox().route('http://a.com', 200).catch(302); + + fetchMock.route('http://b.com', 200).catch(301); + + expect(globalThis.fetch).toEqual(fetchMock.fetchHandler); + expect(fetchMock.fetchHandler).not.toEqual(sbx); + expect(fetchMock.fallbackResponse).not.toEqual(sbx.fallbackResponse); + expect(fetchMock.routes).not.toEqual(sbx.routes); + + const [sandboxed, globally] = await Promise.all([ + sbx('http://a.com'), + fetch('http://b.com'), + ]); + + expect(sandboxed.status).toEqual(200); + expect(globally.status).toEqual(200); + expect(sbx.called('http://a.com')).toBe(true); + expect(sbx.called('http://b.com')).toBe(false); + expect(fetchMock.called('http://b.com')).toBe(true); + expect(fetchMock.called('http://a.com')).toBe(false); + expect(sbx.called('http://a.com')).toBe(true); + fetchMock.restore(); +}); + +describe('global mocking', () => { + let originalFetch; + beforeAll(() => { + originalFetch = globalThis.fetch = vi.fn().mockResolvedValue(); + }); + afterEach(() => fetchMock.restore({ sticky: true })); + + it('global mocking resists resetBehavior calls', () => { + fetchMock.route('*', 200, { sticky: true }).resetBehavior(); + expect(globalThis.fetch).not.toEqual(originalFetch); + }); + + it('global mocking does not resist resetBehavior calls when sent `sticky: true`', () => { + fetchMock + .route('*', 200, { sticky: true }) + .resetBehavior({ sticky: true }); + expect(globalThis.fetch).toEqual(originalFetch); + }); +}); + +describe('sandboxes', () => { + it('sandboxed instances should inherit stickiness', () => { + const sbx1 = fetchMock + .sandbox() + .route('*', 200, { sticky: true }) + .catch(300); + + const sbx2 = sbx1.sandbox().resetBehavior(); + + expect(sbx1.routes.length).toEqual(1); + expect(sbx2.routes.length).toEqual(1); + + sbx2.resetBehavior({ sticky: true }); + + expect(sbx1.routes.length).toEqual(1); + expect(sbx2.routes.length).toEqual(0); + }); +}); + +import { + afterEach, + beforeEach, + describe, + expect, + it, + beforeAll, + vi, +} from 'vitest'; + +const { fetchMock } = testGlobals; +describe('Set up and tear down', () => { + let fm; + beforeAll(() => { + fm = fetchMock.createInstance(); + fm.config.warnOnUnmatched = false; + }); + afterEach(() => fm.restore()); + + const testChainableMethod = (method, ...args) => { + it(`${method}() is chainable`, () => { + expect(fm[method](...args)).toEqual(fm); + }); + + it(`${method}() has "this"`, () => { + vi.spyOn(fm, method).mockReturnThis(); + expect(fm[method](...args)).toBe(fm); + fm[method].mockRestore(); + }); + }; + + describe('mock', () => { + testChainableMethod('mock', '*', 200); + + it('can be called multiple times', () => { + expect(() => { + fm.mock('http://a.com', 200).mock('http://b.com', 200); + }).not.toThrow(); + }); + + it('can be called after fetchMock is restored', () => { + expect(() => { + fm.mock('*', 200).restore().mock('*', 200); + }).not.toThrow(); + }); + + describe('parameters', () => { + beforeEach(() => { + vi.spyOn(fm, 'compileRoute'); + vi.spyOn(fm, '_mock').mockReturnValue(fm); + }); + + afterEach(() => { + fm.compileRoute.mockRestore(); + fm._mock.mockRestore(); + }); + + it('accepts single config object', () => { + const config = { + url: '*', + response: 200, + }; + expect(() => fm.mock(config)).not.toThrow(); + expect(fm.compileRoute).toHaveBeenCalledWith([config]); + expect(fm._mock).toHaveBeenCalled(); + }); + + it('accepts matcher, route pairs', () => { + expect(() => fm.mock('*', 200)).not.toThrow(); + expect(fm.compileRoute).toHaveBeenCalledWith(['*', 200]); + expect(fm._mock).toHaveBeenCalled(); + }); + + it('accepts matcher, response, config triples', () => { + expect(() => + fm.mock('*', 'ok', { + method: 'PUT', + some: 'prop', + }), + ).not.toThrow(); + expect(fm.compileRoute).toHaveBeenCalledWith([ + '*', + 'ok', + { + method: 'PUT', + some: 'prop', + }, + ]); + expect(fm._mock).toHaveBeenCalled(); + }); + + it('expects a matcher', () => { + expect(() => fm.mock(null, 'ok')).toThrow(); + }); + + it('expects a response', () => { + expect(() => fm.mock('*')).toThrow(); + }); + + it('can be called with no parameters', () => { + expect(() => fm.mock()).not.toThrow(); + expect(fm.compileRoute).not.toHaveBeenCalled(); + expect(fm._mock).toHaveBeenCalled(); + }); + + it('should accept object responses when also passing options', () => { + expect(() => + fm.mock('*', { foo: 'bar' }, { method: 'GET' }), + ).not.toThrow(); + }); + }); + }); + + describe('reset', () => { + testChainableMethod('reset'); + + it('can be called even if no mocks set', () => { + expect(() => fm.restore()).not.toThrow(); + }); + + it('calls resetHistory', () => { + vi.spyOn(fm, 'resetHistory'); + fm.restore(); + expect(fm.resetHistory).toHaveBeenCalledTimes(1); + fm.resetHistory.mockRestore(); + }); + + it('removes all routing', () => { + fm.mock('*', 200).catch(200); + + expect(fm.routes.length).toEqual(1); + expect(fm.fallbackResponse).toBeDefined(); + + fm.restore(); + + expect(fm.routes.length).toEqual(0); + expect(fm.fallbackResponse).toBeUndefined(); + }); + + it('restore is an alias for reset', () => { + expect(fm.restore).toEqual(fm.reset); + }); + }); + + describe('resetBehavior', () => { + testChainableMethod('resetBehavior'); + + it('can be called even if no mocks set', () => { + expect(() => fm.resetBehavior()).not.toThrow(); + }); + + it('removes all routing', () => { + fm.mock('*', 200).catch(200); + + expect(fm.routes.length).toEqual(1); + expect(fm.fallbackResponse).toBeDefined(); + + fm.resetBehavior(); + + expect(fm.routes.length).toEqual(0); + expect(fm.fallbackResponse).toBeUndefined(); + }); + }); + + describe('resetHistory', () => { + testChainableMethod('resetHistory'); + + it('can be called even if no mocks set', () => { + expect(() => fm.resetHistory()).not.toThrow(); + }); + + it('resets call history', async () => { + fm.mock('*', 200).catch(200); + await fm.fetchHandler('a'); + await fm.fetchHandler('b'); + expect(fm.called()).toBe(true); + + fm.resetHistory(); + expect(fm.called()).toBe(false); + expect(fm.called('*')).toBe(false); + expect(fm.calls('*').length).toEqual(0); + expect(fm.calls(true).length).toEqual(0); + expect(fm.calls(false).length).toEqual(0); + expect(fm.calls().length).toEqual(0); + }); + }); + + describe('spy', () => { + testChainableMethod('spy'); + + it('calls catch()', () => { + vi.spyOn(fm, 'catch'); + fm.spy(); + expect(fm.catch).toHaveBeenCalledTimes(1); + fm.catch.mockRestore(); + }); + }); +}); + + +import { describe, expect, it, vi } from 'vitest'; + +const { fetchMock } = testGlobals; +describe('spy()', () => { + it('when mocking globally, spy falls through to global fetch', async () => { + const originalFetch = globalThis.fetch; + const fetchSpy = vi.fn().mockResolvedValue('example'); + + globalThis.fetch = fetchSpy; + + fetchMock.spy(); + + await globalThis.fetch('http://a.com/', { method: 'get' }); + expect(fetchSpy).toHaveBeenCalledWith( + 'http://a.com/', + { method: 'get' }, + undefined, + ); + fetchMock.restore(); + globalThis.fetch = originalFetch; + }); + + it('when mocking locally, spy falls through to configured fetch', async () => { + const fetchSpy = vi.fn().mockResolvedValue('dummy'); + + const fm = fetchMock.sandbox(); + fm.config.fetch = fetchSpy; + + fm.spy(); + await fm.fetchHandler('http://a.com/', { method: 'get' }); + expect(fetchSpy).toHaveBeenCalledWith( + 'http://a.com/', + { method: 'get' }, + undefined, + ); + fm.restore(); + }); + + it('can restrict spying to a route', async () => { + const fetchSpy = vi.fn().mockResolvedValue('dummy'); + + const fm = fetchMock.sandbox(); + fm.config.fetch = fetchSpy; + + fm.spy({ url: 'http://a.com/', method: 'get' }); + await fm.fetchHandler('http://a.com/', { method: 'get' }); + expect(fetchSpy).toHaveBeenCalledWith( + 'http://a.com/', + { method: 'get' }, + undefined, + ); + + expect(() => fm.fetchHandler('http://b.com/', { method: 'get' })).toThrow(); + expect(() => + fm.fetchHandler('http://a.com/', { method: 'post' }), + ).toThrow(); + fm.restore(); + }); +}); + + +it('error if spy() is called and no fetch defined in config', () => { + const fm = fetchMock.sandbox(); + delete fm.config.fetch; + expect(() => fm.spy()).toThrow(); +}); + +it("don't error if spy() is called and fetch defined in config", () => { + const fm = fetchMock.sandbox(); + fm.config.fetch = originalFetch; + expect(() => fm.spy()).not.toThrow(); +}); + +it('exports a properly mocked node-fetch module shape', () => { + // uses node-fetch default require pattern + const { + default: fetch, + Headers, + Request, + Response, + } = fetchMock.sandbox(); + + expect(fetch.name).toEqual('fetchMockProxy'); + expect(new Headers()).toBeInstanceOf(fetchMock.config.Headers); + expect(new Request('http://a.com')).toBeInstanceOf( + fetchMock.config.Request, + ); + expect(new Response()).toBeInstanceOf(fetchMock.config.Response); +}); diff --git a/packages/standalone/fallbackToNetwork.test.js b/packages/standalone/fallbackToNetwork.test.js deleted file mode 100644 index 5bef3e8dc..000000000 --- a/packages/standalone/fallbackToNetwork.test.js +++ /dev/null @@ -1,214 +0,0 @@ -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', () => -// new Promise((res) => { -// const readable = new Readable(); -// const write = vi.fn().mockImplementation((chunk, enc, cb) => { -// cb(); -// }); -// const writable = new Writable({ -// write, -// }); -// readable.push('response string'); -// readable.push(null); - -// fetchMock.route(/a/, readable, { sendAsJson: false }); -// fetchMock.fetchHandler('http://a.com').then((res) => { -// res.body.pipe(writable); -// }); - -// writable.on('finish', () => { -// expect(write.args[0][0].toString('utf8')).to.equal('response string'); -// res(); -// }); -// })); - -// // See https://github.com/wheresrhys/fetch-mock/issues/575 -// it('can respond with large bodies from the interweb', async () => { -// const fm = fetchMock.sandbox(); -// fm.config.fallbackToNetwork = true; -// fm.route(); -// // this is an adequate test because the response hangs if the -// // bug referenced above creeps back in -// await fm -// .fetchHandler('http://www.wheresrhys.co.uk/assets/img/chaffinch.jpg') -// .then((res) => res.blob()); -// }); - - - - -// describe.skip('client-side only tests', () => { -// it('not throw when passing unmatched calls through to native fetch', () => { -// fetchMock.config.fallbackToNetwork = true; -// fetchMock.route(); -// expect(() => fetch('http://a.com')).not.to.throw(); -// fetchMock.config.fallbackToNetwork = false; -// }); - -// // this is because we read the body once when normalising the request and -// // want to make sure fetch can still use the sullied request -// it.skip('can send a body on a Request instance when spying ', async () => { -// fetchMock.spy(); -// const req = new fetchMock.config.Request('http://example.com', { -// method: 'post', -// body: JSON.stringify({ prop: 'val' }), -// }); -// try { -// await fetch(req); -// } catch (err) { -// console.log(err); -// expect.unreachable('Fetch should not throw or reject'); -// } -// }); - -// // 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(); -// }); -// } - - - -// it.skip('should cope when there is no global fetch defined', () => { -// const originalFetch = globalThis.fetch; -// delete globalThis.fetch; -// const originalRealFetch = fetchMock.realFetch; -// delete fetchMock.realFetch; -// fetchMock.route('*', 200); -// expect(() => { -// fetch('http://a.com'); -// }).not.to.throw(); - -// expect(() => { -// fetchMock.calls(); -// }).not.to.throw(); -// fetchMock.restore(); -// fetchMock.realFetch = originalRealFetch; -// globalThis.fetch = originalFetch; -// }); - -// if (globalThis.navigator?.serviceWorker) { -// it('should work within a service worker', async () => { -// const registration = -// await globalThis.navigator.serviceWorker.register('__sw.js'); -// await new Promise((resolve, reject) => { -// if (registration.installing) { -// registration.installing.onstatechange = function () { -// if (this.state === 'activated') { -// resolve(); -// } -// }; -// } else { -// reject('No idea what happened'); -// } -// }); - -// await registration.unregister(); -// }); -// } - -// }); - - - - - - diff --git a/packages/standalone/global-fetch.test.js b/packages/standalone/global-fetch.test.js deleted file mode 100644 index e6c7bb2ce..000000000 --- a/packages/standalone/global-fetch.test.js +++ /dev/null @@ -1,136 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -const { fetchMock } = testGlobals; - -describe('use with global fetch', () => { - let originalFetch; - - const expectToBeStubbed = (yes = true) => { - expect(globalThis.fetch).toEqual( - yes ? fetchMock.fetchHandler : originalFetch, - ); - expect(globalThis.fetch).not.toEqual( - yes ? originalFetch : fetchMock.fetchHandler, - ); - }; - - beforeEach(() => { - originalFetch = globalThis.fetch = vi.fn().mockResolvedValue(); - }); - afterEach(fetchMock.restore); - - it('replaces global fetch when mock called', () => { - fetchMock.mock('*', 200); - expectToBeStubbed(); - }); - - it('replaces global fetch when catch called', () => { - fetchMock.catch(200); - expectToBeStubbed(); - }); - - it('replaces global fetch when spy called', () => { - fetchMock.spy(); - expectToBeStubbed(); - }); - - it('restores global fetch after a mock', () => { - fetchMock.mock('*', 200).restore(); - expectToBeStubbed(false); - }); - - it('restores global fetch after a complex mock', () => { - fetchMock.mock('a', 200).mock('b', 200).spy().catch(404).restore(); - expectToBeStubbed(false); - }); - - it('not call default fetch when in mocked mode', async () => { - fetchMock.mock('*', 200); - - await globalThis.fetch('http://a.com'); - expect(originalFetch).not.toHaveBeenCalled(); - }); -}); -let originalFetch; - -beforeAll(() => { - originalFetch = globalThis.fetch = vi.fn().mockResolvedValue('dummy'); -}); - -it('return function', () => { - const sbx = fetchMock.sandbox(); - expect(typeof sbx).toEqual('function'); -}); - - - -it("don't interfere with global fetch", () => { - const sbx = fetchMock.sandbox().route('http://a.com', 200); - - expect(globalThis.fetch).toEqual(originalFetch); - expect(globalThis.fetch).not.toEqual(sbx); -}); - -it("don't interfere with global fetch-mock", async () => { - const sbx = fetchMock.sandbox().route('http://a.com', 200).catch(302); - - fetchMock.route('http://b.com', 200).catch(301); - - expect(globalThis.fetch).toEqual(fetchMock.fetchHandler); - expect(fetchMock.fetchHandler).not.toEqual(sbx); - expect(fetchMock.fallbackResponse).not.toEqual(sbx.fallbackResponse); - expect(fetchMock.routes).not.toEqual(sbx.routes); - - const [sandboxed, globally] = await Promise.all([ - sbx('http://a.com'), - fetch('http://b.com'), - ]); - - expect(sandboxed.status).toEqual(200); - expect(globally.status).toEqual(200); - expect(sbx.called('http://a.com')).toBe(true); - expect(sbx.called('http://b.com')).toBe(false); - expect(fetchMock.called('http://b.com')).toBe(true); - expect(fetchMock.called('http://a.com')).toBe(false); - expect(sbx.called('http://a.com')).toBe(true); - fetchMock.restore(); -}); - -describe('global mocking', () => { - let originalFetch; - beforeAll(() => { - originalFetch = globalThis.fetch = vi.fn().mockResolvedValue(); - }); - afterEach(() => fetchMock.restore({ sticky: true })); - - it('global mocking resists resetBehavior calls', () => { - fetchMock.route('*', 200, { sticky: true }).resetBehavior(); - expect(globalThis.fetch).not.toEqual(originalFetch); - }); - - it('global mocking does not resist resetBehavior calls when sent `sticky: true`', () => { - fetchMock - .route('*', 200, { sticky: true }) - .resetBehavior({ sticky: true }); - expect(globalThis.fetch).toEqual(originalFetch); - }); -}); - -describe('sandboxes', () => { - it('sandboxed instances should inherit stickiness', () => { - const sbx1 = fetchMock - .sandbox() - .route('*', 200, { sticky: true }) - .catch(300); - - const sbx2 = sbx1.sandbox().resetBehavior(); - - expect(sbx1.routes.length).toEqual(1); - expect(sbx2.routes.length).toEqual(1); - - sbx2.resetBehavior({ sticky: true }); - - expect(sbx1.routes.length).toEqual(1); - expect(sbx2.routes.length).toEqual(0); - }); -}); \ No newline at end of file diff --git a/packages/standalone/set-up-and-tear-down.test.js b/packages/standalone/set-up-and-tear-down.test.js deleted file mode 100644 index a4223a3fc..000000000 --- a/packages/standalone/set-up-and-tear-down.test.js +++ /dev/null @@ -1,198 +0,0 @@ -import { - afterEach, - beforeEach, - describe, - expect, - it, - beforeAll, - vi, -} from 'vitest'; - -const { fetchMock } = testGlobals; -describe('Set up and tear down', () => { - let fm; - beforeAll(() => { - fm = fetchMock.createInstance(); - fm.config.warnOnUnmatched = false; - }); - afterEach(() => fm.restore()); - - const testChainableMethod = (method, ...args) => { - it(`${method}() is chainable`, () => { - expect(fm[method](...args)).toEqual(fm); - }); - - it(`${method}() has "this"`, () => { - vi.spyOn(fm, method).mockReturnThis(); - expect(fm[method](...args)).toBe(fm); - fm[method].mockRestore(); - }); - }; - - describe('mock', () => { - testChainableMethod('mock', '*', 200); - - it('can be called multiple times', () => { - expect(() => { - fm.mock('http://a.com', 200).mock('http://b.com', 200); - }).not.toThrow(); - }); - - it('can be called after fetchMock is restored', () => { - expect(() => { - fm.mock('*', 200).restore().mock('*', 200); - }).not.toThrow(); - }); - - describe('parameters', () => { - beforeEach(() => { - vi.spyOn(fm, 'compileRoute'); - vi.spyOn(fm, '_mock').mockReturnValue(fm); - }); - - afterEach(() => { - fm.compileRoute.mockRestore(); - fm._mock.mockRestore(); - }); - - it('accepts single config object', () => { - const config = { - url: '*', - response: 200, - }; - expect(() => fm.mock(config)).not.toThrow(); - expect(fm.compileRoute).toHaveBeenCalledWith([config]); - expect(fm._mock).toHaveBeenCalled(); - }); - - it('accepts matcher, route pairs', () => { - expect(() => fm.mock('*', 200)).not.toThrow(); - expect(fm.compileRoute).toHaveBeenCalledWith(['*', 200]); - expect(fm._mock).toHaveBeenCalled(); - }); - - it('accepts matcher, response, config triples', () => { - expect(() => - fm.mock('*', 'ok', { - method: 'PUT', - some: 'prop', - }), - ).not.toThrow(); - expect(fm.compileRoute).toHaveBeenCalledWith([ - '*', - 'ok', - { - method: 'PUT', - some: 'prop', - }, - ]); - expect(fm._mock).toHaveBeenCalled(); - }); - - it('expects a matcher', () => { - expect(() => fm.mock(null, 'ok')).toThrow(); - }); - - it('expects a response', () => { - expect(() => fm.mock('*')).toThrow(); - }); - - it('can be called with no parameters', () => { - expect(() => fm.mock()).not.toThrow(); - expect(fm.compileRoute).not.toHaveBeenCalled(); - expect(fm._mock).toHaveBeenCalled(); - }); - - it('should accept object responses when also passing options', () => { - expect(() => - fm.mock('*', { foo: 'bar' }, { method: 'GET' }), - ).not.toThrow(); - }); - }); - }); - - describe('reset', () => { - testChainableMethod('reset'); - - it('can be called even if no mocks set', () => { - expect(() => fm.restore()).not.toThrow(); - }); - - it('calls resetHistory', () => { - vi.spyOn(fm, 'resetHistory'); - fm.restore(); - expect(fm.resetHistory).toHaveBeenCalledTimes(1); - fm.resetHistory.mockRestore(); - }); - - it('removes all routing', () => { - fm.mock('*', 200).catch(200); - - expect(fm.routes.length).toEqual(1); - expect(fm.fallbackResponse).toBeDefined(); - - fm.restore(); - - expect(fm.routes.length).toEqual(0); - expect(fm.fallbackResponse).toBeUndefined(); - }); - - it('restore is an alias for reset', () => { - expect(fm.restore).toEqual(fm.reset); - }); - }); - - describe('resetBehavior', () => { - testChainableMethod('resetBehavior'); - - it('can be called even if no mocks set', () => { - expect(() => fm.resetBehavior()).not.toThrow(); - }); - - it('removes all routing', () => { - fm.mock('*', 200).catch(200); - - expect(fm.routes.length).toEqual(1); - expect(fm.fallbackResponse).toBeDefined(); - - fm.resetBehavior(); - - expect(fm.routes.length).toEqual(0); - expect(fm.fallbackResponse).toBeUndefined(); - }); - }); - - describe('resetHistory', () => { - testChainableMethod('resetHistory'); - - it('can be called even if no mocks set', () => { - expect(() => fm.resetHistory()).not.toThrow(); - }); - - it('resets call history', async () => { - fm.mock('*', 200).catch(200); - await fm.fetchHandler('a'); - await fm.fetchHandler('b'); - expect(fm.called()).toBe(true); - - fm.resetHistory(); - expect(fm.called()).toBe(false); - expect(fm.called('*')).toBe(false); - expect(fm.calls('*').length).toEqual(0); - expect(fm.calls(true).length).toEqual(0); - expect(fm.calls(false).length).toEqual(0); - expect(fm.calls().length).toEqual(0); - }); - }); - - describe('spy', () => { - testChainableMethod('spy'); - - it('calls catch()', () => { - vi.spyOn(fm, 'catch'); - fm.spy(); - expect(fm.catch).toHaveBeenCalledTimes(1); - fm.catch.mockRestore(); - }); - }); -}); diff --git a/packages/standalone/spy.test.js b/packages/standalone/spy.test.js deleted file mode 100644 index f8fa63345..000000000 --- a/packages/standalone/spy.test.js +++ /dev/null @@ -1,89 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; - -const { fetchMock } = testGlobals; -describe('spy()', () => { - it('when mocking globally, spy falls through to global fetch', async () => { - const originalFetch = globalThis.fetch; - const fetchSpy = vi.fn().mockResolvedValue('example'); - - globalThis.fetch = fetchSpy; - - fetchMock.spy(); - - await globalThis.fetch('http://a.com/', { method: 'get' }); - expect(fetchSpy).toHaveBeenCalledWith( - 'http://a.com/', - { method: 'get' }, - undefined, - ); - fetchMock.restore(); - globalThis.fetch = originalFetch; - }); - - it('when mocking locally, spy falls through to configured fetch', async () => { - const fetchSpy = vi.fn().mockResolvedValue('dummy'); - - const fm = fetchMock.sandbox(); - fm.config.fetch = fetchSpy; - - fm.spy(); - await fm.fetchHandler('http://a.com/', { method: 'get' }); - expect(fetchSpy).toHaveBeenCalledWith( - 'http://a.com/', - { method: 'get' }, - undefined, - ); - fm.restore(); - }); - - it('can restrict spying to a route', async () => { - const fetchSpy = vi.fn().mockResolvedValue('dummy'); - - const fm = fetchMock.sandbox(); - fm.config.fetch = fetchSpy; - - fm.spy({ url: 'http://a.com/', method: 'get' }); - await fm.fetchHandler('http://a.com/', { method: 'get' }); - expect(fetchSpy).toHaveBeenCalledWith( - 'http://a.com/', - { method: 'get' }, - undefined, - ); - - expect(() => fm.fetchHandler('http://b.com/', { method: 'get' })).toThrow(); - expect(() => - fm.fetchHandler('http://a.com/', { method: 'post' }), - ).toThrow(); - fm.restore(); - }); -}); - - -it('error if spy() is called and no fetch defined in config', () => { - const fm = fetchMock.sandbox(); - delete fm.config.fetch; - expect(() => fm.spy()).toThrow(); -}); - -it("don't error if spy() is called and fetch defined in config", () => { - const fm = fetchMock.sandbox(); - fm.config.fetch = originalFetch; - expect(() => fm.spy()).not.toThrow(); -}); - -it('exports a properly mocked node-fetch module shape', () => { - // uses node-fetch default require pattern - const { - default: fetch, - Headers, - Request, - Response, - } = fetchMock.sandbox(); - - expect(fetch.name).toEqual('fetchMockProxy'); - expect(new Headers()).toBeInstanceOf(fetchMock.config.Headers); - expect(new Request('http://a.com')).toBeInstanceOf( - fetchMock.config.Request, - ); - expect(new Response()).toBeInstanceOf(fetchMock.config.Response); -}); \ No newline at end of file diff --git a/packages/wip/generated-types/CallHistory.d.ts b/packages/wip/generated-types/CallHistory.d.ts deleted file mode 100644 index b4cc425bc..000000000 --- a/packages/wip/generated-types/CallHistory.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -export default FetchMock; -declare namespace FetchMock { - export function filterCalls(nameOrMatcher: any, options: any): any; - export function calls(nameOrMatcher: any, options: any): any; - export function lastCall(nameOrMatcher: any, options: any): any; - export function lastUrl(nameOrMatcher: any, options: any): any; - export function lastOptions(nameOrMatcher: any, options: any): any; - export function lastResponse(nameOrMatcher: any, options: any): any; - export function called(nameOrMatcher: any, options: any): boolean; - export function done(nameOrMatcher: any): any; -} diff --git a/packages/wip/generated-types/FetchHandler.d.ts b/packages/wip/generated-types/FetchHandler.d.ts deleted file mode 100644 index b345c68d7..000000000 --- a/packages/wip/generated-types/FetchHandler.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -export default FetchHandler; -/** - * An object that contains the fetch handler function - used as the mock for - * fetch - and various utilities to help it operate - * This object will never be accessed as a separate entity by the end user as it - * gets munged with Router and CallHistory objects by FetchMockWrapper - */ -export type FetchHandler = any; -declare namespace FetchHandler { - export function fetchHandler(url: any, options: any): Promise; - export namespace fetchHandler { - export const isMock: boolean; - } - export function generateResponse({ route, url, options, request, callLog, }: { - route: any; - }): Promise; -} diff --git a/packages/wip/generated-types/FetchMockWrapper.d.ts b/packages/wip/generated-types/FetchMockWrapper.d.ts deleted file mode 100644 index e384567ab..000000000 --- a/packages/wip/generated-types/FetchMockWrapper.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare var _default: any; -export default _default; diff --git a/packages/wip/generated-types/Matchers.d.ts b/packages/wip/generated-types/Matchers.d.ts deleted file mode 100644 index 2197135de..000000000 --- a/packages/wip/generated-types/Matchers.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -declare var _default: ({ - name: string; - matcher: (route: any) => any; - usesBody: boolean; -} | { - name: string; - matcher: ({ functionMatcher }: { - functionMatcher: any; - }) => (...args: any[]) => any; - usesBody?: undefined; -} | { - name: string; - matcher: (route: any) => any; - usesBody?: undefined; -})[]; -export default _default; diff --git a/packages/wip/generated-types/RequestUtils.d.ts b/packages/wip/generated-types/RequestUtils.d.ts deleted file mode 100644 index 314012f1e..000000000 --- a/packages/wip/generated-types/RequestUtils.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -export function normalizeUrl(url: any): any; -/** - * - * @param {string|Request} urlOrRequest - * @param {Object} options - * @param {Class} Request - * @returns - */ -export function normalizeRequest(urlOrRequest: string | Request, options: Object, Request: any): { - url: any; - options: { - method: any; - } & Object; - request: RequestInfo; - signal: any; -} | { - url: any; - options: Object; - signal: any; -}; -export function getPath(url: any): string; -export function getQuery(url: any): string; -export namespace headers { - export function normalize(headers: any): any; - export function toLowerCase(headers: any): {}; - export function equal(actualHeader: any, expectedHeader: any): any; -} diff --git a/packages/wip/generated-types/ResponseBuilder.d.ts b/packages/wip/generated-types/ResponseBuilder.d.ts deleted file mode 100644 index 27e419068..000000000 --- a/packages/wip/generated-types/ResponseBuilder.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare function _default(options: any): any[]; -export default _default; diff --git a/packages/wip/generated-types/Route.d.ts b/packages/wip/generated-types/Route.d.ts deleted file mode 100644 index 1516d4326..000000000 --- a/packages/wip/generated-types/Route.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -export default Route; -declare class Route { - /** - * @param {MatcherDefinition} matcher - */ - static defineMatcher(matcher: any): void; - /** - * @overload - * @param {MockOptions} matcher - * @param {undefined} response - * @param {undefined} options - * @param {FetchMockConfig} globalConfig - */ - /** - * @overload - * @param {MockMatcher } matcher - * @param {MockResponse} response - * @param {MockOptions | string} options - * @param {FetchMockConfig} globalConfig - */ - /** - * @param {MockMatcher | MockOptions} matcher - * @param {MockResponse} [response] - * @param {MockOptions | string} [options] - * @param {FetchMockConfig} [globalConfig] - */ - constructor(matcher: any | any, response?: any, options?: any | string, globalConfig?: any); - originalInput: { - matcher: any; - response: any; - options: any; - }; - method: any; - url: (url: any, options: {}, request: any) => boolean; - functionMatcher: any; - usesBody: boolean; - matcher: (url: any, options: {}, request: any) => boolean; - reset: () => void; - response: () => Promise; - #private; -} -declare namespace Route { - export const registeredMatchers: any[]; -} diff --git a/packages/wip/generated-types/Router.d.ts b/packages/wip/generated-types/Router.d.ts deleted file mode 100644 index 3ec2ae8ba..000000000 --- a/packages/wip/generated-types/Router.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare var routes: any; -declare function defineShorthand(methodName: any, underlyingMethod: any, shorthandOptions: any): void; -declare function defineGreedyShorthand(methodName: any, underlyingMethod: any): void; diff --git a/packages/wip/generated-types/StatusTextMap.d.ts b/packages/wip/generated-types/StatusTextMap.d.ts deleted file mode 100644 index b98f5db67..000000000 --- a/packages/wip/generated-types/StatusTextMap.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default statusTextMap; -/** - * @type {Object.} - */ -declare const statusTextMap: { - [x: number]: string; -}; diff --git a/packages/wip/generated-types/__tests__/CallHistory.test.d.ts b/packages/wip/generated-types/__tests__/CallHistory.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/CallHistory.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/FetchHandler.test.d.ts b/packages/wip/generated-types/__tests__/FetchHandler.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/FetchHandler.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/FetchMockWrapper.test.d.ts b/packages/wip/generated-types/__tests__/FetchMockWrapper.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/FetchMockWrapper.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Matchers.test.d.ts b/packages/wip/generated-types/__tests__/Matchers.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Matchers.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/ResponseBuilder.test.d.ts b/packages/wip/generated-types/__tests__/ResponseBuilder.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/ResponseBuilder.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Router/Router.test.d.ts b/packages/wip/generated-types/__tests__/Router/Router.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Router/Router.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Router/body-matching.test.d.ts b/packages/wip/generated-types/__tests__/Router/body-matching.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Router/body-matching.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Router/edge-cases.test.d.ts b/packages/wip/generated-types/__tests__/Router/edge-cases.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Router/edge-cases.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Router/function-matching.test.d.ts b/packages/wip/generated-types/__tests__/Router/function-matching.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Router/function-matching.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Router/header-matching.test.d.ts b/packages/wip/generated-types/__tests__/Router/header-matching.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Router/header-matching.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Router/matchPartialBody.test.d.ts b/packages/wip/generated-types/__tests__/Router/matchPartialBody.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Router/matchPartialBody.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Router/matcher-object.test.d.ts b/packages/wip/generated-types/__tests__/Router/matcher-object.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Router/matcher-object.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Router/method-matching.test.d.ts b/packages/wip/generated-types/__tests__/Router/method-matching.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Router/method-matching.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Router/multiple-routes.test.d.ts b/packages/wip/generated-types/__tests__/Router/multiple-routes.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Router/multiple-routes.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Router/naming-routes.test.d.ts b/packages/wip/generated-types/__tests__/Router/naming-routes.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Router/naming-routes.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Router/path-parameter-matching.test.d.ts b/packages/wip/generated-types/__tests__/Router/path-parameter-matching.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Router/path-parameter-matching.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Router/query-string-matching.test.d.ts b/packages/wip/generated-types/__tests__/Router/query-string-matching.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Router/query-string-matching.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Router/sticky-routes.test.d.ts b/packages/wip/generated-types/__tests__/Router/sticky-routes.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Router/sticky-routes.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Router/unmatched-calls.test.d.ts b/packages/wip/generated-types/__tests__/Router/unmatched-calls.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Router/unmatched-calls.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/generated-types/__tests__/Router/url-matching.test.d.ts b/packages/wip/generated-types/__tests__/Router/url-matching.test.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/wip/generated-types/__tests__/Router/url-matching.test.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/wip/old-types/CallHistory.d.ts b/packages/wip/old-types/CallHistory.d.ts deleted file mode 100644 index b77845cb2..000000000 --- a/packages/wip/old-types/CallHistory.d.ts +++ /dev/null @@ -1,159 +0,0 @@ -export default FetchMock; -declare namespace FetchMock { - export function filterCalls(nameOrMatcher: any, options: any): any; - export function calls(nameOrMatcher: any, options: any): any; - export function lastCall(nameOrMatcher: any, options: any): any; - export function lastUrl(nameOrMatcher: any, options: any): any; - export function lastOptions(nameOrMatcher: any, options: any): any; - export function lastResponse(nameOrMatcher: any, options: any): any; - export function called(nameOrMatcher: any, options: any): boolean; - export function done(nameOrMatcher: any): any; -} - -interface MockCall extends Array { - 0: string; - 1: RequestInit | undefined; - identifier: string; - isUnmatched: boolean | undefined; - request: Request | undefined; - response: Response | undefined; -} - - -/** - * Returns a promise that resolves once all fetches handled by fetch-mock - * have resolved. - * @param [waitForBody] Wait for all body parsing methods(res.json(), - * res.text(), etc.) to resolve too. - */ -flush(waitForBody?: boolean): Promise; - -/** - * Inspection filter. Can be one of the following: - * boolean: - * * true retrieves all calls matched by fetch. - * fetchMock.MATCHED is an alias for true and may be used to make tests - * more readable. - * * false retrieves all calls not matched by fetch (i.e. those handled - * by catch() or spy(). fetchMock.UNMATCHED is an alias for false and - * may be used to make tests more readable. - * MockMatcher (routeIdentifier): - * All routes have an identifier: - * * If it’s a named route, the identifier is the route’s name - * * If the route is unnamed, the identifier is the matcher passed in to - * .mock() - * All calls that were handled by the route with the given identifier - * will be retrieved - * MockMatcher (matcher): - * Any matcher compatible with the mocking api can be passed in to filter - * the calls arbitrarily. - */ -type InspectionFilter = MockMatcher | boolean; - -/** - * Either an object compatible with the mocking api or a string specifying - * a http method to filter by. This will be used to filter the list of - * calls further. - */ -type InspectionOptions = MockOptions | string; - - - -// /** -// * Returns an array of all calls to fetch matching the given filters. -// * Each call is returned as a [url, options] array. If fetch was called -// * using a Request instance, this will be available as a request -// * property on this array. -// * @param [filter] Allows filtering of calls to fetch based on various -// * criteria -// * @param [options] Either an object compatible with the mocking api or -// * a string specifying a http method to filter by. This will be used to -// * filter the list of calls further. -// */ -// calls(filter?: InspectionFilter, options?: InspectionOptions): MockCall[]; - -// /** -// * Returns a Boolean indicating whether any calls to fetch matched the -// * given filter. -// * @param [filter] Allows filtering of calls to fetch based on various -// * criteria -// * @param [options] Either an object compatible with the mocking api or -// * a string specifying a http method to filter by. This will be used to -// * filter the list of calls further. -// */ -// called(filter?: InspectionFilter, options?: InspectionOptions): boolean; - -// /** -// * Returns a Boolean indicating whether fetch was called the expected -// * number of times (or has been called at least once if repeat is -// * undefined for the route). -// * @param [filter] Rule for matching calls to fetch. -// */ -// done(filter?: InspectionFilter): boolean; - -// /** -// * Returns the arguments for the last call to fetch matching the given -// * filter. -// * @param [filter] Allows filtering of calls to fetch based on various -// * criteria -// * @param [options] Either an object compatible with the mocking api or -// * a string specifying a http method to filter by. This will be used to -// * filter the list of calls further. -// */ -// lastCall( -// filter?: InspectionFilter, -// options?: InspectionOptions, -// ): MockCall | undefined; - -// /** -// * Returns the url for the last call to fetch matching the given -// * filter. If fetch was last called using a Request instance, the url -// * will be extracted from this. -// * @param [filter] Allows filtering of calls to fetch based on various -// * criteria -// * @param [options] Either an object compatible with the mocking api or -// * a string specifying a http method to filter by. This will be used to -// * filter the list of calls further. -// */ -// lastUrl( -// filter?: InspectionFilter, -// options?: InspectionOptions, -// ): string | undefined; - -// /** -// * Returns the options for the call to fetch matching the given filter. -// * If fetch was last called using a Request instance, a set of options -// * inferred from the Request will be returned. -// * @param [filter] Allows filtering of calls to fetch based on various -// * criteria -// * @param [options] Either an object compatible with the mocking api or -// * a string specifying a http method to filter by. This will be used to -// * filter the list of calls further. -// */ -// lastOptions( -// filter?: InspectionFilter, -// options?: InspectionOptions, -// ): MockOptions | undefined; - -// /** -// * Returns the options for the call to fetch matching the given filter. -// * This is an experimental feature, very difficult to implement well given -// * fetch’s very private treatment of response bodies. -// * When doing all the following: -// - using node-fetch -// - responding with a real network response (using spy() or fallbackToNetwork) -// - using `fetchMock.LastResponse()` -// - awaiting the body content -// … the response will hang unless your source code also awaits the response body. -// This is an unavoidable consequence of the nodejs implementation of streams. -// * @param [filter] Allows filtering of calls to fetch based on various -// * criteria -// * @param [options] Either an object compatible with the mocking api or -// * a string specifying a http method to filter by. This will be used to -// * filter the list of calls further. -// */ -// lastResponse( -// filter?: InspectionFilter, -// options?: InspectionOptions, -// ): Response | undefined; - diff --git a/packages/wip/old-types/FetchMockWrapper.d.ts b/packages/wip/old-types/FetchMockWrapper.d.ts deleted file mode 100644 index 0d313fa15..000000000 --- a/packages/wip/old-types/FetchMockWrapper.d.ts +++ /dev/null @@ -1,84 +0,0 @@ -interface FetchMockConfig { - - /** - * Convert objects into JSON before delivering as stub responses. - * Can be useful to set to false globally if e.g. dealing with a - * lot of array buffers. If true, will also add - * content-type: application/json header. - * @default true - */ - sendAsJson?: boolean; - - /** - * Automatically sets a content-length header on each response. - * @default true - */ - includeContentLength?: boolean; - - // /** - // * - true: Unhandled calls fall through to the network - // * - false: Unhandled calls throw an error - // * - 'always': All calls fall through to the network, effectively - // * disabling fetch-mock. - // * @default false - // */ - // fallbackToNetwork?: boolean | 'always'; - - /** - * Print a warning if any call is caught by a fallback handler (set - * using the fallbackToNetwork option or catch()) - * @default true - */ - warnOnFallback?: boolean; - - /** - * Reference to a custom fetch implementation. - */ - fetch?: ( - input?: string | Request, - init?: RequestInit, - ) => Promise; - - /** - * Reference to the Headers constructor of a custom fetch - * implementation. - */ - Headers?: new () => Headers; - - /** - * Reference to the Request constructor of a custom fetch - * implementation. - */ - Request?: new (input: string | Request, init?: RequestInit) => Request; - - /** - * Reference to the Response constructor of a custom fetch - * implementation. - */ - Response?: new () => Response; -} - - - - -interface FetchMockInstance { - - // MATCHED: true; - // UNMATCHED: false; - - - - /** - * Returns a promise that resolves once all fetches handled by fetch-mock - * have resolved. - * @param [waitForBody] Wait for all body parsing methods(res.json(), - * res.text(), etc.) to resolve too. - */ - flush(waitForBody?: boolean): Promise; - - statusTextMap: { - [key: number]: string - } - - config: FetchMockConfig; -} \ No newline at end of file diff --git a/packages/wip/old-types/RequestUtils.d.ts b/packages/wip/old-types/RequestUtils.d.ts deleted file mode 100644 index 314012f1e..000000000 --- a/packages/wip/old-types/RequestUtils.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -export function normalizeUrl(url: any): any; -/** - * - * @param {string|Request} urlOrRequest - * @param {Object} options - * @param {Class} Request - * @returns - */ -export function normalizeRequest(urlOrRequest: string | Request, options: Object, Request: any): { - url: any; - options: { - method: any; - } & Object; - request: RequestInfo; - signal: any; -} | { - url: any; - options: Object; - signal: any; -}; -export function getPath(url: any): string; -export function getQuery(url: any): string; -export namespace headers { - export function normalize(headers: any): any; - export function toLowerCase(headers: any): {}; - export function equal(actualHeader: any, expectedHeader: any): any; -} diff --git a/packages/wip/old-types/ResponseBuilder.d.ts b/packages/wip/old-types/ResponseBuilder.d.ts deleted file mode 100644 index 4f5665d80..000000000 --- a/packages/wip/old-types/ResponseBuilder.d.ts +++ /dev/null @@ -1,54 +0,0 @@ - -/** - * Mock response object - */ -interface MockResponseObject { - /** - * Set the response body - */ - body?: string | {}; - - /** - * Set the response status - * @default 200 - */ - status?: number; - - /** - * Set the response headers. - */ - headers?: { [key: string]: string }; - - /** - * If this property is present then a Promise rejected with the value - * of throws is returned - */ - throws?: Error; - - /** - * The URL the response should be from (to imitate followed redirects - * - will set redirected: true on the response) - */ - redirectUrl?: string; -} - -/** - * Response: A Response instance - will be used unaltered - * number: Creates a response with this status - * string: Creates a 200 response with the string as the response body - * object: As long as the object is not a MockResponseObject it is - * converted into a json string and returned as the body of a 200 response - * If MockResponseObject was given then it's used to configure response - * Function(url, opts): A function that is passed the url and opts fetch() - * is called with and that returns any of the responses listed above - */ -type MockResponse = Response | Promise - | number | Promise - | string | Promise - | {} | Promise<{}> - | MockResponseObject | Promise; - -/** - * Mock response function - */ -type MockResponseFunction = (url: string, opts: MockRequest) => MockResponse; diff --git a/packages/wip/old-types/StatusTextMap.d.ts b/packages/wip/old-types/StatusTextMap.d.ts deleted file mode 100644 index b98f5db67..000000000 --- a/packages/wip/old-types/StatusTextMap.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default statusTextMap; -/** - * @type {Object.} - */ -declare const statusTextMap: { - [x: number]: string; -}; diff --git a/packages/wip/old-types/index.d.ts b/packages/wip/old-types/index.d.ts deleted file mode 100644 index 3ed4b3f81..000000000 --- a/packages/wip/old-types/index.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// TypeScript Version: 2.2 -import Route from "./Route"; -import CallHistory from "./CallHistory"; -import FetchHandler from "./FetchHandler"; -import FetchMockWrapper from "./FetchMockWrapper"; -import RequestUtils from "./RequestUtils"; -import ResponseBuilder from "./ResponseBuilder"; -import Router from "./Router"; -import StatusTextMap from "./StatusTextMap"; - - - - - - - From a9638fc12f60bfa28e6169a9fa736e2bbdc21a8a Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Thu, 25 Jul 2024 17:13:33 +0100 Subject: [PATCH 5/9] feat: allow spying on just one route --- .../@fetch-mock/core/mocking-and-spying.md | 29 ++++- docs/docs/@fetch-mock/core/resetting.md | 4 + packages/core/src/FetchMock.js | 24 +++++ .../__tests__/FetchMock/mock-and-spy.test.js | 100 ++---------------- 4 files changed, 61 insertions(+), 96 deletions(-) diff --git a/docs/docs/@fetch-mock/core/mocking-and-spying.md b/docs/docs/@fetch-mock/core/mocking-and-spying.md index 42e8ed253..ee9ac010b 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 98072f320..43ae4b014 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 5547d34b7..158a5167c 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 636190eee..3cc354f4a 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(); // }); -// } From cdd20184df7b5b93b6fab341b1bc9562ce3f3303 Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Thu, 25 Jul 2024 17:53:35 +0100 Subject: [PATCH 6/9] refactor: refined the spying api --- .../@fetch-mock/core/mocking-and-spying.md | 29 +- packages/core/src/FetchMock.js | 45 +- .../__tests__/FetchMock/mock-and-spy.test.js | 897 +++++++++--------- packages/core/types/FetchMock.d.ts | 3 +- 4 files changed, 476 insertions(+), 498 deletions(-) diff --git a/docs/docs/@fetch-mock/core/mocking-and-spying.md b/docs/docs/@fetch-mock/core/mocking-and-spying.md index ee9ac010b..516d1375c 100644 --- a/docs/docs/@fetch-mock/core/mocking-and-spying.md +++ b/docs/docs/@fetch-mock/core/mocking-and-spying.md @@ -16,33 +16,14 @@ In addition to the @fetch-mock/core API its methods are: ### mockGlobal() -Replaces `fetch` with `fm.fetchHandler` - -### 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). +Replaces `globalThis.fetch` with `fm.fetchHandler` ### restoreGlobal() -Restores `fetch` to its original state - - -## 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) +Restores `globalThis.fetch` to its original state -Fall back to the provided `fetch` implementation for any calls unmatched by a route. +### spy(matcher, name) -## setfetchImplementation(fetchImplementation) +Falls back to the `fetch` implementation set in `fetchMock.config.fetch` for a specific route (which can be named). -When you wish to use `.spyRoute()` use this function first to provide a `fetch` implementation to use. +When no arguments are provided it will fallback to the native fetch implementation for all requests, similar to `.catch()` diff --git a/packages/core/src/FetchMock.js b/packages/core/src/FetchMock.js index 158a5167c..49de936b4 100644 --- a/packages/core/src/FetchMock.js +++ b/packages/core/src/FetchMock.js @@ -228,7 +228,6 @@ class FetchMockStandalone extends FetchMock { * @this {FetchMockStandalone} */ mockGlobal() { - this.#originalFetch = globalThis.fetch; globalThis.fetch = this.fetchHandler.bind(this); return this; } @@ -236,51 +235,23 @@ class FetchMockStandalone extends FetchMock { * @this {FetchMockStandalone} */ restoreGlobal() { - globalThis.fetch = this.#originalFetch; - return this; - } - /** - * @this {FetchMockStandalone} - */ - spyGlobal() { - this.#originalFetch = globalThis.fetch; - globalThis.fetch = this.fetchHandler.bind(this); - // @ts-ignore - this.catch(({ args }) => this.#originalFetch(...args)); + globalThis.fetch = this.config.fetch; return this; } /** - * @param {RouteMatcher | UserRouteConfig} matcher + * @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') + spy(matcher, name) { + if (matcher) { + // @ts-ignore + this.route(matcher, ({args}) => this.config.fetch(...args), name); + } else { + this.catch(({ args }) => this.config.fetch(...args)); } - // @ts-ignore - this.route(matcher, ({args}) => this.#originalFetch(...args), name); - return this; - } - /** - * @param {typeof fetch} fetchImplementation - * @this {FetchMockStandalone} - */ - spyLocal(fetchImplementation) { - this.#originalFetch = fetchImplementation; - // @ts-ignore - this.catch(({ args }) => this.#originalFetch(...args)); - return this; - } - - /** - * @param {typeof fetch} fetchImplementation - * @this {FetchMockStandalone} - */ - setFetchImplementation(fetchImplementation) { - this.#originalFetch = fetchImplementation; return this; } 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 3cc354f4a..e37481028 100644 --- a/packages/core/src/__tests__/FetchMock/mock-and-spy.test.js +++ b/packages/core/src/__tests__/FetchMock/mock-and-spy.test.js @@ -1,54 +1,86 @@ -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; -const { fetchMock } = testGlobals; +import fetchMock from '../../FetchMock.js' +describe('mock and spy', () => { + let fm; + const nativeFetch = globalThis.fetch; + beforeEach(() => { + fm = fetchMock.createInstance() + }) + afterEach(() => { + globalThis.fetch = nativeFetch; + }) -// // only works in node-fetch@2 -// it.skip('can respond with a readable stream', () => -// new Promise((res) => { -// const readable = new Readable(); -// const write = vi.fn().mockImplementation((chunk, enc, cb) => { -// cb(); -// }); -// const writable = new Writable({ -// write, -// }); -// readable.push('response string'); -// readable.push(null); + const testChainableMethod = (method, ...args) => { + it(`${method}() is chainable`, () => { + expect(fm[method](...args)).toEqual(fm); + }); -// fetchMock.route(/a/, readable, { sendAsJson: false }); -// fetchMock.fetchHandler('http://a.com').then((res) => { -// res.body.pipe(writable); -// }); + it(`${method}() has "this"`, () => { + vi.spyOn(fm, method).mockReturnThis(); + expect(fm[method](...args)).toBe(fm); + fm[method].mockRestore(); + }); + }; -// writable.on('finish', () => { -// expect(write.args[0][0].toString('utf8')).to.equal('response string'); -// res(); -// }); -// })); + describe('.mockGlobal()', () => { + testChainableMethod('mockGlobal') + testChainableMethod('restoreGlobal') + it('replaces global fetch with fetchMock.fetchHandler', () => { + fm.mockGlobal() + expect(globalThis.fetch).toEqual(fm.fetchHandler) + }) + + it('calls to fetch are successfully handled by fetchMock.fetchHandler', async () => { + fm.mockGlobal() + .catch(200); + const response = await fetch('https://a.com', {method: 'post'}); + expect(response.status).toEqual(200); + const callLog = fm.callHistory.lastCall(); + expect(callLog.args).toEqual( [ 'https://a.com/', { method: 'post' } ]) + }) + + it('restores global fetch', () => { + fm.mockGlobal().restoreGlobal(); + expect(globalThis.fetch).toEqual(nativeFetch) + }) -// // See https://github.com/wheresrhys/fetch-mock/issues/575 -// it('can respond with large bodies from the interweb', async () => { -// const fm = fetchMock.sandbox(); -// fm.config.fallbackToNetwork = true; -// fm.route(); -// // this is an adequate test because the response hangs if the -// // bug referenced above creeps back in -// await fm -// .fetchHandler('http://www.wheresrhys.co.uk/assets/img/chaffinch.jpg') -// .then((res) => res.blob()); -// }); + }) + describe('.spy()', () => { + testChainableMethod('spyGlobal') + it('passes all requests through to the network by default', () => {}) + it('falls through to global fetch for a specific route', () => { -describe('mock and spy', () => { + }) + it('can apply the full range of matchers and route options', () => { - describe('.mockGlobal()', () => { + }) - }) - describe('.spyGlobal()', () => { + it('can name a route', () => { + + }) + + it('plays nice with mockGlobal()', () => {}) + // vi.spyOn(globalThis, 'fetch') + // fm.spyGlobal() + // try { + // await fetch('https://a.com', {method: 'post'}); + // } catch (err) {} + // expect(globalThis.fetch).toHaveBeenCalledWith('https://a.com', {method: 'post'}) + // const callLog = fm.callHistory.lastCall(); + // expect(callLog.args).toEqual( [ 'https://a.com/', { method: 'post' } ]) + // globalThis.fetch.restore() + // }) + // it('restores global fetch', () => { + // fm.spyGlobal().restoreGlobal(); + // expect(globalThis.fetch).toEqual(nativeFetch) + // }) }) + }) // describe.skip('client-side only tests', () => { // it('not throw when passing unmatched calls through to native fetch', () => { @@ -130,429 +162,424 @@ describe('mock and spy', () => { -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +// import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -const { fetchMock } = testGlobals; +// const { fetchMock } = testGlobals; -describe('use with global fetch', () => { - let originalFetch; +// describe('use with global fetch', () => { +// let originalFetch; - const expectToBeStubbed = (yes = true) => { - expect(globalThis.fetch).toEqual( - yes ? fetchMock.fetchHandler : originalFetch, - ); - expect(globalThis.fetch).not.toEqual( - yes ? originalFetch : fetchMock.fetchHandler, - ); - }; +// const expectToBeStubbed = (yes = true) => { +// expect(globalThis.fetch).toEqual( +// yes ? fetchMock.fetchHandler : originalFetch, +// ); +// expect(globalThis.fetch).not.toEqual( +// yes ? originalFetch : fetchMock.fetchHandler, +// ); +// }; - beforeEach(() => { - originalFetch = globalThis.fetch = vi.fn().mockResolvedValue(); - }); - afterEach(fetchMock.restore); - - it('replaces global fetch when mock called', () => { - fetchMock.mock('*', 200); - expectToBeStubbed(); - }); - - it('replaces global fetch when catch called', () => { - fetchMock.catch(200); - expectToBeStubbed(); - }); - - it('replaces global fetch when spy called', () => { - fetchMock.spy(); - expectToBeStubbed(); - }); - - it('restores global fetch after a mock', () => { - fetchMock.mock('*', 200).restore(); - expectToBeStubbed(false); - }); - - it('restores global fetch after a complex mock', () => { - fetchMock.mock('a', 200).mock('b', 200).spy().catch(404).restore(); - expectToBeStubbed(false); - }); - - it('not call default fetch when in mocked mode', async () => { - fetchMock.mock('*', 200); - - await globalThis.fetch('http://a.com'); - expect(originalFetch).not.toHaveBeenCalled(); - }); -}); -let originalFetch; - -beforeAll(() => { - originalFetch = globalThis.fetch = vi.fn().mockResolvedValue('dummy'); -}); - -it('return function', () => { - const sbx = fetchMock.sandbox(); - expect(typeof sbx).toEqual('function'); -}); - - - -it("don't interfere with global fetch", () => { - const sbx = fetchMock.sandbox().route('http://a.com', 200); - - expect(globalThis.fetch).toEqual(originalFetch); - expect(globalThis.fetch).not.toEqual(sbx); -}); - -it("don't interfere with global fetch-mock", async () => { - const sbx = fetchMock.sandbox().route('http://a.com', 200).catch(302); - - fetchMock.route('http://b.com', 200).catch(301); - - expect(globalThis.fetch).toEqual(fetchMock.fetchHandler); - expect(fetchMock.fetchHandler).not.toEqual(sbx); - expect(fetchMock.fallbackResponse).not.toEqual(sbx.fallbackResponse); - expect(fetchMock.routes).not.toEqual(sbx.routes); - - const [sandboxed, globally] = await Promise.all([ - sbx('http://a.com'), - fetch('http://b.com'), - ]); - - expect(sandboxed.status).toEqual(200); - expect(globally.status).toEqual(200); - expect(sbx.called('http://a.com')).toBe(true); - expect(sbx.called('http://b.com')).toBe(false); - expect(fetchMock.called('http://b.com')).toBe(true); - expect(fetchMock.called('http://a.com')).toBe(false); - expect(sbx.called('http://a.com')).toBe(true); - fetchMock.restore(); -}); - -describe('global mocking', () => { - let originalFetch; - beforeAll(() => { - originalFetch = globalThis.fetch = vi.fn().mockResolvedValue(); - }); - afterEach(() => fetchMock.restore({ sticky: true })); - - it('global mocking resists resetBehavior calls', () => { - fetchMock.route('*', 200, { sticky: true }).resetBehavior(); - expect(globalThis.fetch).not.toEqual(originalFetch); - }); - - it('global mocking does not resist resetBehavior calls when sent `sticky: true`', () => { - fetchMock - .route('*', 200, { sticky: true }) - .resetBehavior({ sticky: true }); - expect(globalThis.fetch).toEqual(originalFetch); - }); -}); - -describe('sandboxes', () => { - it('sandboxed instances should inherit stickiness', () => { - const sbx1 = fetchMock - .sandbox() - .route('*', 200, { sticky: true }) - .catch(300); - - const sbx2 = sbx1.sandbox().resetBehavior(); - - expect(sbx1.routes.length).toEqual(1); - expect(sbx2.routes.length).toEqual(1); - - sbx2.resetBehavior({ sticky: true }); - - expect(sbx1.routes.length).toEqual(1); - expect(sbx2.routes.length).toEqual(0); - }); -}); - -import { - afterEach, - beforeEach, - describe, - expect, - it, - beforeAll, - vi, -} from 'vitest'; - -const { fetchMock } = testGlobals; -describe('Set up and tear down', () => { - let fm; - beforeAll(() => { - fm = fetchMock.createInstance(); - fm.config.warnOnUnmatched = false; - }); - afterEach(() => fm.restore()); +// beforeEach(() => { +// originalFetch = globalThis.fetch = vi.fn().mockResolvedValue(); +// }); +// afterEach(fetchMock.restore); - const testChainableMethod = (method, ...args) => { - it(`${method}() is chainable`, () => { - expect(fm[method](...args)).toEqual(fm); - }); +// it('replaces global fetch when mock called', () => { +// fetchMock.mock('*', 200); +// expectToBeStubbed(); +// }); - it(`${method}() has "this"`, () => { - vi.spyOn(fm, method).mockReturnThis(); - expect(fm[method](...args)).toBe(fm); - fm[method].mockRestore(); - }); - }; +// it('replaces global fetch when catch called', () => { +// fetchMock.catch(200); +// expectToBeStubbed(); +// }); - describe('mock', () => { - testChainableMethod('mock', '*', 200); +// it('replaces global fetch when spy called', () => { +// fetchMock.spy(); +// expectToBeStubbed(); +// }); - it('can be called multiple times', () => { - expect(() => { - fm.mock('http://a.com', 200).mock('http://b.com', 200); - }).not.toThrow(); - }); +// it('restores global fetch after a mock', () => { +// fetchMock.mock('*', 200).restore(); +// expectToBeStubbed(false); +// }); - it('can be called after fetchMock is restored', () => { - expect(() => { - fm.mock('*', 200).restore().mock('*', 200); - }).not.toThrow(); - }); +// it('restores global fetch after a complex mock', () => { +// fetchMock.mock('a', 200).mock('b', 200).spy().catch(404).restore(); +// expectToBeStubbed(false); +// }); - describe('parameters', () => { - beforeEach(() => { - vi.spyOn(fm, 'compileRoute'); - vi.spyOn(fm, '_mock').mockReturnValue(fm); - }); - - afterEach(() => { - fm.compileRoute.mockRestore(); - fm._mock.mockRestore(); - }); - - it('accepts single config object', () => { - const config = { - url: '*', - response: 200, - }; - expect(() => fm.mock(config)).not.toThrow(); - expect(fm.compileRoute).toHaveBeenCalledWith([config]); - expect(fm._mock).toHaveBeenCalled(); - }); - - it('accepts matcher, route pairs', () => { - expect(() => fm.mock('*', 200)).not.toThrow(); - expect(fm.compileRoute).toHaveBeenCalledWith(['*', 200]); - expect(fm._mock).toHaveBeenCalled(); - }); - - it('accepts matcher, response, config triples', () => { - expect(() => - fm.mock('*', 'ok', { - method: 'PUT', - some: 'prop', - }), - ).not.toThrow(); - expect(fm.compileRoute).toHaveBeenCalledWith([ - '*', - 'ok', - { - method: 'PUT', - some: 'prop', - }, - ]); - expect(fm._mock).toHaveBeenCalled(); - }); - - it('expects a matcher', () => { - expect(() => fm.mock(null, 'ok')).toThrow(); - }); - - it('expects a response', () => { - expect(() => fm.mock('*')).toThrow(); - }); - - it('can be called with no parameters', () => { - expect(() => fm.mock()).not.toThrow(); - expect(fm.compileRoute).not.toHaveBeenCalled(); - expect(fm._mock).toHaveBeenCalled(); - }); - - it('should accept object responses when also passing options', () => { - expect(() => - fm.mock('*', { foo: 'bar' }, { method: 'GET' }), - ).not.toThrow(); - }); - }); - }); +// it('not call default fetch when in mocked mode', async () => { +// fetchMock.mock('*', 200); - describe('reset', () => { - testChainableMethod('reset'); +// await globalThis.fetch('http://a.com'); +// expect(originalFetch).not.toHaveBeenCalled(); +// }); +// }); +// let originalFetch; - it('can be called even if no mocks set', () => { - expect(() => fm.restore()).not.toThrow(); - }); +// beforeAll(() => { +// originalFetch = globalThis.fetch = vi.fn().mockResolvedValue('dummy'); +// }); - it('calls resetHistory', () => { - vi.spyOn(fm, 'resetHistory'); - fm.restore(); - expect(fm.resetHistory).toHaveBeenCalledTimes(1); - fm.resetHistory.mockRestore(); - }); +// it('return function', () => { +// const sbx = fetchMock.sandbox(); +// expect(typeof sbx).toEqual('function'); +// }); - it('removes all routing', () => { - fm.mock('*', 200).catch(200); - expect(fm.routes.length).toEqual(1); - expect(fm.fallbackResponse).toBeDefined(); - fm.restore(); +// it("don't interfere with global fetch", () => { +// const sbx = fetchMock.sandbox().route('http://a.com', 200); - expect(fm.routes.length).toEqual(0); - expect(fm.fallbackResponse).toBeUndefined(); - }); +// expect(globalThis.fetch).toEqual(originalFetch); +// expect(globalThis.fetch).not.toEqual(sbx); +// }); - it('restore is an alias for reset', () => { - expect(fm.restore).toEqual(fm.reset); - }); - }); +// it("don't interfere with global fetch-mock", async () => { +// const sbx = fetchMock.sandbox().route('http://a.com', 200).catch(302); + +// fetchMock.route('http://b.com', 200).catch(301); + +// expect(globalThis.fetch).toEqual(fetchMock.fetchHandler); +// expect(fetchMock.fetchHandler).not.toEqual(sbx); +// expect(fetchMock.fallbackResponse).not.toEqual(sbx.fallbackResponse); +// expect(fetchMock.routes).not.toEqual(sbx.routes); + +// const [sandboxed, globally] = await Promise.all([ +// sbx('http://a.com'), +// fetch('http://b.com'), +// ]); + +// expect(sandboxed.status).toEqual(200); +// expect(globally.status).toEqual(200); +// expect(sbx.called('http://a.com')).toBe(true); +// expect(sbx.called('http://b.com')).toBe(false); +// expect(fetchMock.called('http://b.com')).toBe(true); +// expect(fetchMock.called('http://a.com')).toBe(false); +// expect(sbx.called('http://a.com')).toBe(true); +// fetchMock.restore(); +// }); - describe('resetBehavior', () => { - testChainableMethod('resetBehavior'); +// describe('global mocking', () => { +// let originalFetch; +// beforeAll(() => { +// originalFetch = globalThis.fetch = vi.fn().mockResolvedValue(); +// }); +// afterEach(() => fetchMock.restore({ sticky: true })); + +// it('global mocking resists resetBehavior calls', () => { +// fetchMock.route('*', 200, { sticky: true }).resetBehavior(); +// expect(globalThis.fetch).not.toEqual(originalFetch); +// }); + +// it('global mocking does not resist resetBehavior calls when sent `sticky: true`', () => { +// fetchMock +// .route('*', 200, { sticky: true }) +// .resetBehavior({ sticky: true }); +// expect(globalThis.fetch).toEqual(originalFetch); +// }); +// }); - it('can be called even if no mocks set', () => { - expect(() => fm.resetBehavior()).not.toThrow(); - }); +// describe('sandboxes', () => { +// it('sandboxed instances should inherit stickiness', () => { +// const sbx1 = fetchMock +// .sandbox() +// .route('*', 200, { sticky: true }) +// .catch(300); - it('removes all routing', () => { - fm.mock('*', 200).catch(200); +// const sbx2 = sbx1.sandbox().resetBehavior(); - expect(fm.routes.length).toEqual(1); - expect(fm.fallbackResponse).toBeDefined(); +// expect(sbx1.routes.length).toEqual(1); +// expect(sbx2.routes.length).toEqual(1); - fm.resetBehavior(); +// sbx2.resetBehavior({ sticky: true }); - expect(fm.routes.length).toEqual(0); - expect(fm.fallbackResponse).toBeUndefined(); - }); - }); +// expect(sbx1.routes.length).toEqual(1); +// expect(sbx2.routes.length).toEqual(0); +// }); +// }); - describe('resetHistory', () => { - testChainableMethod('resetHistory'); +// import { +// afterEach, +// beforeEach, +// describe, +// expect, +// it, +// beforeAll, +// vi, +// } from 'vitest'; + +// const { fetchMock } = testGlobals; +// describe('Set up and tear down', () => { +// let fm; +// beforeAll(() => { +// fm = fetchMock.createInstance(); +// fm.config.warnOnUnmatched = false; +// }); +// afterEach(() => fm.restore()); + +// const testChainableMethod = (method, ...args) => { +// it(`${method}() is chainable`, () => { +// expect(fm[method](...args)).toEqual(fm); +// }); + +// it(`${method}() has "this"`, () => { +// vi.spyOn(fm, method).mockReturnThis(); +// expect(fm[method](...args)).toBe(fm); +// fm[method].mockRestore(); +// }); +// }; + +// describe('mock', () => { +// testChainableMethod('mock', '*', 200); + +// it('can be called multiple times', () => { +// expect(() => { +// fm.mock('http://a.com', 200).mock('http://b.com', 200); +// }).not.toThrow(); +// }); + +// it('can be called after fetchMock is restored', () => { +// expect(() => { +// fm.mock('*', 200).restore().mock('*', 200); +// }).not.toThrow(); +// }); + +// describe('parameters', () => { +// beforeEach(() => { +// vi.spyOn(fm, 'compileRoute'); +// vi.spyOn(fm, '_mock').mockReturnValue(fm); +// }); + +// afterEach(() => { +// fm.compileRoute.mockRestore(); +// fm._mock.mockRestore(); +// }); + +// it('accepts single config object', () => { +// const config = { +// url: '*', +// response: 200, +// }; +// expect(() => fm.mock(config)).not.toThrow(); +// expect(fm.compileRoute).toHaveBeenCalledWith([config]); +// expect(fm._mock).toHaveBeenCalled(); +// }); + +// it('accepts matcher, route pairs', () => { +// expect(() => fm.mock('*', 200)).not.toThrow(); +// expect(fm.compileRoute).toHaveBeenCalledWith(['*', 200]); +// expect(fm._mock).toHaveBeenCalled(); +// }); + +// it('accepts matcher, response, config triples', () => { +// expect(() => +// fm.mock('*', 'ok', { +// method: 'PUT', +// some: 'prop', +// }), +// ).not.toThrow(); +// expect(fm.compileRoute).toHaveBeenCalledWith([ +// '*', +// 'ok', +// { +// method: 'PUT', +// some: 'prop', +// }, +// ]); +// expect(fm._mock).toHaveBeenCalled(); +// }); + +// it('expects a matcher', () => { +// expect(() => fm.mock(null, 'ok')).toThrow(); +// }); + +// it('expects a response', () => { +// expect(() => fm.mock('*')).toThrow(); +// }); + +// it('can be called with no parameters', () => { +// expect(() => fm.mock()).not.toThrow(); +// expect(fm.compileRoute).not.toHaveBeenCalled(); +// expect(fm._mock).toHaveBeenCalled(); +// }); + +// it('should accept object responses when also passing options', () => { +// expect(() => +// fm.mock('*', { foo: 'bar' }, { method: 'GET' }), +// ).not.toThrow(); +// }); +// }); +// }); + +// describe('reset', () => { +// testChainableMethod('reset'); + +// it('can be called even if no mocks set', () => { +// expect(() => fm.restore()).not.toThrow(); +// }); + +// it('calls resetHistory', () => { +// vi.spyOn(fm, 'resetHistory'); +// fm.restore(); +// expect(fm.resetHistory).toHaveBeenCalledTimes(1); +// fm.resetHistory.mockRestore(); +// }); + +// it('removes all routing', () => { +// fm.mock('*', 200).catch(200); + +// expect(fm.routes.length).toEqual(1); +// expect(fm.fallbackResponse).toBeDefined(); + +// fm.restore(); + +// expect(fm.routes.length).toEqual(0); +// expect(fm.fallbackResponse).toBeUndefined(); +// }); + +// it('restore is an alias for reset', () => { +// expect(fm.restore).toEqual(fm.reset); +// }); +// }); + + +// describe('spy', () => { +// testChainableMethod('spy'); + +// it('calls catch()', () => { +// vi.spyOn(fm, 'catch'); +// fm.spy(); +// expect(fm.catch).toHaveBeenCalledTimes(1); +// fm.catch.mockRestore(); +// }); +// }); +// }); - it('can be called even if no mocks set', () => { - expect(() => fm.resetHistory()).not.toThrow(); - }); - it('resets call history', async () => { - fm.mock('*', 200).catch(200); - await fm.fetchHandler('a'); - await fm.fetchHandler('b'); - expect(fm.called()).toBe(true); - - fm.resetHistory(); - expect(fm.called()).toBe(false); - expect(fm.called('*')).toBe(false); - expect(fm.calls('*').length).toEqual(0); - expect(fm.calls(true).length).toEqual(0); - expect(fm.calls(false).length).toEqual(0); - expect(fm.calls().length).toEqual(0); - }); - }); +// import { describe, expect, it, vi } from 'vitest'; + +// const { fetchMock } = testGlobals; +// describe('spy()', () => { +// it('when mocking globally, spy falls through to global fetch', async () => { +// const originalFetch = globalThis.fetch; +// const fetchSpy = vi.fn().mockResolvedValue('example'); + +// globalThis.fetch = fetchSpy; + +// fetchMock.spy(); + +// await globalThis.fetch('http://a.com/', { method: 'get' }); +// expect(fetchSpy).toHaveBeenCalledWith( +// 'http://a.com/', +// { method: 'get' }, +// undefined, +// ); +// fetchMock.restore(); +// globalThis.fetch = originalFetch; +// }); + +// it('when mocking locally, spy falls through to configured fetch', async () => { +// const fetchSpy = vi.fn().mockResolvedValue('dummy'); + +// const fm = fetchMock.sandbox(); +// fm.config.fetch = fetchSpy; + +// fm.spy(); +// await fm.fetchHandler('http://a.com/', { method: 'get' }); +// expect(fetchSpy).toHaveBeenCalledWith( +// 'http://a.com/', +// { method: 'get' }, +// undefined, +// ); +// fm.restore(); +// }); + +// it('can restrict spying to a route', async () => { +// const fetchSpy = vi.fn().mockResolvedValue('dummy'); + +// const fm = fetchMock.sandbox(); +// fm.config.fetch = fetchSpy; + +// fm.spy({ url: 'http://a.com/', method: 'get' }); +// await fm.fetchHandler('http://a.com/', { method: 'get' }); +// expect(fetchSpy).toHaveBeenCalledWith( +// 'http://a.com/', +// { method: 'get' }, +// undefined, +// ); + +// expect(() => fm.fetchHandler('http://b.com/', { method: 'get' })).toThrow(); +// expect(() => +// fm.fetchHandler('http://a.com/', { method: 'post' }), +// ).toThrow(); +// fm.restore(); +// }); +// }); - describe('spy', () => { - testChainableMethod('spy'); - it('calls catch()', () => { - vi.spyOn(fm, 'catch'); - fm.spy(); - expect(fm.catch).toHaveBeenCalledTimes(1); - fm.catch.mockRestore(); - }); - }); -}); - - -import { describe, expect, it, vi } from 'vitest'; - -const { fetchMock } = testGlobals; -describe('spy()', () => { - it('when mocking globally, spy falls through to global fetch', async () => { - const originalFetch = globalThis.fetch; - const fetchSpy = vi.fn().mockResolvedValue('example'); - - globalThis.fetch = fetchSpy; - - fetchMock.spy(); - - await globalThis.fetch('http://a.com/', { method: 'get' }); - expect(fetchSpy).toHaveBeenCalledWith( - 'http://a.com/', - { method: 'get' }, - undefined, - ); - fetchMock.restore(); - globalThis.fetch = originalFetch; - }); - - it('when mocking locally, spy falls through to configured fetch', async () => { - const fetchSpy = vi.fn().mockResolvedValue('dummy'); - - const fm = fetchMock.sandbox(); - fm.config.fetch = fetchSpy; - - fm.spy(); - await fm.fetchHandler('http://a.com/', { method: 'get' }); - expect(fetchSpy).toHaveBeenCalledWith( - 'http://a.com/', - { method: 'get' }, - undefined, - ); - fm.restore(); - }); - - it('can restrict spying to a route', async () => { - const fetchSpy = vi.fn().mockResolvedValue('dummy'); - - const fm = fetchMock.sandbox(); - fm.config.fetch = fetchSpy; - - fm.spy({ url: 'http://a.com/', method: 'get' }); - await fm.fetchHandler('http://a.com/', { method: 'get' }); - expect(fetchSpy).toHaveBeenCalledWith( - 'http://a.com/', - { method: 'get' }, - undefined, - ); - - expect(() => fm.fetchHandler('http://b.com/', { method: 'get' })).toThrow(); - expect(() => - fm.fetchHandler('http://a.com/', { method: 'post' }), - ).toThrow(); - fm.restore(); - }); -}); - - -it('error if spy() is called and no fetch defined in config', () => { - const fm = fetchMock.sandbox(); - delete fm.config.fetch; - expect(() => fm.spy()).toThrow(); -}); - -it("don't error if spy() is called and fetch defined in config", () => { - const fm = fetchMock.sandbox(); - fm.config.fetch = originalFetch; - expect(() => fm.spy()).not.toThrow(); -}); - -it('exports a properly mocked node-fetch module shape', () => { - // uses node-fetch default require pattern - const { - default: fetch, - Headers, - Request, - Response, - } = fetchMock.sandbox(); - - expect(fetch.name).toEqual('fetchMockProxy'); - expect(new Headers()).toBeInstanceOf(fetchMock.config.Headers); - expect(new Request('http://a.com')).toBeInstanceOf( - fetchMock.config.Request, - ); - expect(new Response()).toBeInstanceOf(fetchMock.config.Response); -}); +// it('error if spy() is called and no fetch defined in config', () => { +// const fm = fetchMock.sandbox(); +// delete fm.config.fetch; +// expect(() => fm.spy()).toThrow(); +// }); + +// it("don't error if spy() is called and fetch defined in config", () => { +// const fm = fetchMock.sandbox(); +// fm.config.fetch = originalFetch; +// expect(() => fm.spy()).not.toThrow(); +// }); + +// it('exports a properly mocked node-fetch module shape', () => { +// // uses node-fetch default require pattern +// const { +// default: fetch, +// Headers, +// Request, +// Response, +// } = fetchMock.sandbox(); + +// expect(fetch.name).toEqual('fetchMockProxy'); +// expect(new Headers()).toBeInstanceOf(fetchMock.config.Headers); +// expect(new Request('http://a.com')).toBeInstanceOf( +// fetchMock.config.Request, +// ); +// expect(new Response()).toBeInstanceOf(fetchMock.config.Response); +// }); + + +// // only works in node-fetch@2 +// it.skip('can respond with a readable stream', () => +// new Promise((res) => { +// const readable = new Readable(); +// const write = vi.fn().mockImplementation((chunk, enc, cb) => { +// cb(); +// }); +// const writable = new Writable({ +// write, +// }); +// readable.push('response string'); +// readable.push(null); + +// fetchMock.route(/a/, readable, { sendAsJson: false }); +// fetchMock.fetchHandler('http://a.com').then((res) => { +// res.body.pipe(writable); +// }); + +// writable.on('finish', () => { +// expect(write.args[0][0].toString('utf8')).to.equal('response string'); +// res(); +// }); +// })); + +// // See https://github.com/wheresrhys/fetch-mock/issues/575 +// it('can respond with large bodies from the interweb', async () => { +// const fm = fetchMock.sandbox(); +// fm.config.fallbackToNetwork = true; +// fm.route(); +// // this is an adequate test because the response hangs if the +// // bug referenced above creeps back in +// await fm +// .fetchHandler('http://www.wheresrhys.co.uk/assets/img/chaffinch.jpg') +// .then((res) => res.blob()); +// }); diff --git a/packages/core/types/FetchMock.d.ts b/packages/core/types/FetchMock.d.ts index eea8ffe7f..8312a5add 100644 --- a/packages/core/types/FetchMock.d.ts +++ b/packages/core/types/FetchMock.d.ts @@ -100,8 +100,7 @@ declare const fetchMock: FetchMockStandalone; declare class FetchMockStandalone extends FetchMock { mockGlobal(this: FetchMockStandalone): FetchMockStandalone; restoreGlobal(this: FetchMockStandalone): FetchMockStandalone; - spyGlobal(this: FetchMockStandalone): FetchMockStandalone; - spyLocal(this: FetchMockStandalone, fetchImplementation: typeof fetch): FetchMockStandalone; + spy(this: FetchMockStandalone, matcher?: RouteMatcher | UserRouteConfig, name?: RouteName): FetchMockStandalone; createInstance(): FetchMockStandalone; #private; } From 3ad4241f409353ac970cf26b1252b32ea6390208 Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Fri, 26 Jul 2024 11:45:53 +0100 Subject: [PATCH 7/9] feat: rename restoreGlobal to unmockGlobal --- .../@fetch-mock/core/mocking-and-spying.md | 8 +- docs/docs/@fetch-mock/core/resetting.md | 2 +- packages/core/src/FetchMock.js | 14 +- packages/core/src/Router.js | 1 - .../__tests__/FetchMock/mock-and-spy.test.js | 649 +++--------------- .../src/__tests__/router-integration.test.js | 81 +++ packages/core/types/FetchMock.d.ts | 2 +- 7 files changed, 198 insertions(+), 559 deletions(-) diff --git a/docs/docs/@fetch-mock/core/mocking-and-spying.md b/docs/docs/@fetch-mock/core/mocking-and-spying.md index 516d1375c..8c9efa441 100644 --- a/docs/docs/@fetch-mock/core/mocking-and-spying.md +++ b/docs/docs/@fetch-mock/core/mocking-and-spying.md @@ -18,12 +18,16 @@ In addition to the @fetch-mock/core API its methods are: Replaces `globalThis.fetch` with `fm.fetchHandler` -### restoreGlobal() +### unmockGlobal() Restores `globalThis.fetch` to its original state ### spy(matcher, name) -Falls back to the `fetch` implementation set in `fetchMock.config.fetch` for a specific route (which can be named). +Falls back to the `fetch` implementation set in `fetchMock.config.fetch` for a specific route (which can be named). When no arguments are provided it will fallback to the native fetch implementation for all requests, similar to `.catch()` + +### spyGlobal() + +Equivalent to calling `.mockGlobal()` followed by `.spy()` diff --git a/docs/docs/@fetch-mock/core/resetting.md b/docs/docs/@fetch-mock/core/resetting.md index 43ae4b014..06cebfc34 100644 --- a/docs/docs/@fetch-mock/core/resetting.md +++ b/docs/docs/@fetch-mock/core/resetting.md @@ -26,7 +26,7 @@ A boolean indicating whether or not to remove the fallback route (added using `. Clears all data recorded for `fetch`'s calls. -## restoreGlobal() +## unmockGlobal() Restores global `fetch` to its original state if `.mockGlobal()` or `.spyGlobal()` have been used . diff --git a/packages/core/src/FetchMock.js b/packages/core/src/FetchMock.js index 49de936b4..3151906e0 100644 --- a/packages/core/src/FetchMock.js +++ b/packages/core/src/FetchMock.js @@ -20,7 +20,7 @@ import * as requestUtils from './RequestUtils.js'; /** * @typedef FetchImplementations - * @property {function(string | Request, RequestInit): Promise} [fetch] + * @property {typeof fetch} [fetch] * @property {typeof Headers} [Headers] * @property {typeof Request} [Request] * @property {typeof Response} [Response] @@ -234,7 +234,7 @@ class FetchMockStandalone extends FetchMock { /** * @this {FetchMockStandalone} */ - restoreGlobal() { + unmockGlobal() { globalThis.fetch = this.config.fetch; return this; } @@ -247,13 +247,21 @@ class FetchMockStandalone extends FetchMock { spy(matcher, name) { if (matcher) { // @ts-ignore - this.route(matcher, ({args}) => this.config.fetch(...args), name); + this.route(matcher, ({ args }) => this.config.fetch(...args), name); } else { + // @ts-ignore this.catch(({ args }) => this.config.fetch(...args)); } return this; } + /** + * @this {FetchMockStandalone} + */ + spyGlobal() { + this.mockGlobal(); + return this.spy(); + } createInstance() { return new FetchMockStandalone({ ...this.config }, this.router); diff --git a/packages/core/src/Router.js b/packages/core/src/Router.js index da82d9645..16415180a 100644 --- a/packages/core/src/Router.js +++ b/packages/core/src/Router.js @@ -185,7 +185,6 @@ export default class Router { ? [...this.routes, this.fallbackRoute] : this.routes; const route = routesToTry.find((route) => route.matcher(callLog)); - if (route) { try { callLog.route = route; 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 e37481028..2793960ff 100644 --- a/packages/core/src/__tests__/FetchMock/mock-and-spy.test.js +++ b/packages/core/src/__tests__/FetchMock/mock-and-spy.test.js @@ -1,16 +1,15 @@ import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; - -import fetchMock from '../../FetchMock.js' +import fetchMock from '../../FetchMock.js'; describe('mock and spy', () => { let fm; const nativeFetch = globalThis.fetch; beforeEach(() => { - fm = fetchMock.createInstance() - }) + fm = fetchMock.createInstance(); + }); afterEach(() => { globalThis.fetch = nativeFetch; - }) + }); const testChainableMethod = (method, ...args) => { it(`${method}() is chainable`, () => { @@ -25,561 +24,109 @@ describe('mock and spy', () => { }; describe('.mockGlobal()', () => { - testChainableMethod('mockGlobal') - testChainableMethod('restoreGlobal') + testChainableMethod('mockGlobal'); + testChainableMethod('unmockGlobal'); + it('replaces global fetch with fetchMock.fetchHandler', () => { - fm.mockGlobal() - expect(globalThis.fetch).toEqual(fm.fetchHandler) - }) + vi.spyOn(fm, 'fetchHandler'); + fm.mockGlobal(); + fetch('http://a.com', { method: 'post' }); + // cannot just check globalThis.fetch === fm.fetchHandler because we apply .bind() to fetchHandler + expect(fm.fetchHandler).toHaveBeenCalledWith('http://a.com', { + method: 'post', + }); + }); it('calls to fetch are successfully handled by fetchMock.fetchHandler', async () => { - fm.mockGlobal() - .catch(200); - const response = await fetch('https://a.com', {method: 'post'}); + fm.mockGlobal().catch(200); + const response = await fetch('http://a.com', { method: 'post' }); expect(response.status).toEqual(200); const callLog = fm.callHistory.lastCall(); - expect(callLog.args).toEqual( [ 'https://a.com/', { method: 'post' } ]) - }) + expect(callLog.args).toEqual(['http://a.com/', { method: 'post' }]); + }); it('restores global fetch', () => { - fm.mockGlobal().restoreGlobal(); - expect(globalThis.fetch).toEqual(nativeFetch) - }) - - }) + fm.mockGlobal().unmockGlobal(); + expect(globalThis.fetch).toEqual(nativeFetch); + }); + }); describe('.spy()', () => { - testChainableMethod('spyGlobal') - it('passes all requests through to the network by default', () => {}) - it('falls through to global fetch for a specific route', () => { - - }) - - it('can apply the full range of matchers and route options', () => { - - }) - - it('can name a route', () => { - - }) - - it('plays nice with mockGlobal()', () => {}) - // vi.spyOn(globalThis, 'fetch') - // fm.spyGlobal() - // try { - // await fetch('https://a.com', {method: 'post'}); - // } catch (err) {} - // expect(globalThis.fetch).toHaveBeenCalledWith('https://a.com', {method: 'post'}) - // const callLog = fm.callHistory.lastCall(); - // expect(callLog.args).toEqual( [ 'https://a.com/', { method: 'post' } ]) - // globalThis.fetch.restore() - // }) - - // it('restores global fetch', () => { - // fm.spyGlobal().restoreGlobal(); - // expect(globalThis.fetch).toEqual(nativeFetch) - // }) - }) - - -}) -// describe.skip('client-side only tests', () => { -// it('not throw when passing unmatched calls through to native fetch', () => { -// fetchMock.config.fallbackToNetwork = true; -// fetchMock.route(); -// expect(() => fetch('http://a.com')).not.to.throw(); -// fetchMock.config.fallbackToNetwork = false; -// }); - -// // this is because we read the body once when normalising the request and -// // want to make sure fetch can still use the sullied request -// it.skip('can send a body on a Request instance when spying ', async () => { -// fetchMock.spy(); -// const req = new fetchMock.config.Request('http://example.com', { -// method: 'post', -// body: JSON.stringify({ prop: 'val' }), -// }); -// try { -// await fetch(req); -// } catch (err) { -// console.log(err); -// expect.unreachable('Fetch should not throw or reject'); -// } -// }); - - -// 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(); -// }); - - - -// it.skip('should cope when there is no global fetch defined', () => { -// const originalFetch = globalThis.fetch; -// delete globalThis.fetch; -// const originalRealFetch = fetchMock.realFetch; -// delete fetchMock.realFetch; -// fetchMock.route('*', 200); -// expect(() => { -// fetch('http://a.com'); -// }).not.to.throw(); - -// expect(() => { -// fetchMock.calls(); -// }).not.to.throw(); -// fetchMock.restore(); -// fetchMock.realFetch = originalRealFetch; -// globalThis.fetch = originalFetch; -// }); - -// if (globalThis.navigator?.serviceWorker) { -// it('should work within a service worker', async () => { -// const registration = -// await globalThis.navigator.serviceWorker.register('__sw.js'); -// await new Promise((resolve, reject) => { -// if (registration.installing) { -// registration.installing.onstatechange = function () { -// if (this.state === 'activated') { -// resolve(); -// } -// }; -// } else { -// reject('No idea what happened'); -// } -// }); - -// await registration.unregister(); -// }); -// } - -// }); - - - - - - -// import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -// const { fetchMock } = testGlobals; - -// describe('use with global fetch', () => { -// let originalFetch; - -// const expectToBeStubbed = (yes = true) => { -// expect(globalThis.fetch).toEqual( -// yes ? fetchMock.fetchHandler : originalFetch, -// ); -// expect(globalThis.fetch).not.toEqual( -// yes ? originalFetch : fetchMock.fetchHandler, -// ); -// }; - -// beforeEach(() => { -// originalFetch = globalThis.fetch = vi.fn().mockResolvedValue(); -// }); -// afterEach(fetchMock.restore); - -// it('replaces global fetch when mock called', () => { -// fetchMock.mock('*', 200); -// expectToBeStubbed(); -// }); - -// it('replaces global fetch when catch called', () => { -// fetchMock.catch(200); -// expectToBeStubbed(); -// }); - -// it('replaces global fetch when spy called', () => { -// fetchMock.spy(); -// expectToBeStubbed(); -// }); - -// it('restores global fetch after a mock', () => { -// fetchMock.mock('*', 200).restore(); -// expectToBeStubbed(false); -// }); - -// it('restores global fetch after a complex mock', () => { -// fetchMock.mock('a', 200).mock('b', 200).spy().catch(404).restore(); -// expectToBeStubbed(false); -// }); - -// it('not call default fetch when in mocked mode', async () => { -// fetchMock.mock('*', 200); - -// await globalThis.fetch('http://a.com'); -// expect(originalFetch).not.toHaveBeenCalled(); -// }); -// }); -// let originalFetch; - -// beforeAll(() => { -// originalFetch = globalThis.fetch = vi.fn().mockResolvedValue('dummy'); -// }); - -// it('return function', () => { -// const sbx = fetchMock.sandbox(); -// expect(typeof sbx).toEqual('function'); -// }); - - - -// it("don't interfere with global fetch", () => { -// const sbx = fetchMock.sandbox().route('http://a.com', 200); - -// expect(globalThis.fetch).toEqual(originalFetch); -// expect(globalThis.fetch).not.toEqual(sbx); -// }); - -// it("don't interfere with global fetch-mock", async () => { -// const sbx = fetchMock.sandbox().route('http://a.com', 200).catch(302); - -// fetchMock.route('http://b.com', 200).catch(301); - -// expect(globalThis.fetch).toEqual(fetchMock.fetchHandler); -// expect(fetchMock.fetchHandler).not.toEqual(sbx); -// expect(fetchMock.fallbackResponse).not.toEqual(sbx.fallbackResponse); -// expect(fetchMock.routes).not.toEqual(sbx.routes); - -// const [sandboxed, globally] = await Promise.all([ -// sbx('http://a.com'), -// fetch('http://b.com'), -// ]); - -// expect(sandboxed.status).toEqual(200); -// expect(globally.status).toEqual(200); -// expect(sbx.called('http://a.com')).toBe(true); -// expect(sbx.called('http://b.com')).toBe(false); -// expect(fetchMock.called('http://b.com')).toBe(true); -// expect(fetchMock.called('http://a.com')).toBe(false); -// expect(sbx.called('http://a.com')).toBe(true); -// fetchMock.restore(); -// }); - -// describe('global mocking', () => { -// let originalFetch; -// beforeAll(() => { -// originalFetch = globalThis.fetch = vi.fn().mockResolvedValue(); -// }); -// afterEach(() => fetchMock.restore({ sticky: true })); - -// it('global mocking resists resetBehavior calls', () => { -// fetchMock.route('*', 200, { sticky: true }).resetBehavior(); -// expect(globalThis.fetch).not.toEqual(originalFetch); -// }); - -// it('global mocking does not resist resetBehavior calls when sent `sticky: true`', () => { -// fetchMock -// .route('*', 200, { sticky: true }) -// .resetBehavior({ sticky: true }); -// expect(globalThis.fetch).toEqual(originalFetch); -// }); -// }); - -// describe('sandboxes', () => { -// it('sandboxed instances should inherit stickiness', () => { -// const sbx1 = fetchMock -// .sandbox() -// .route('*', 200, { sticky: true }) -// .catch(300); - -// const sbx2 = sbx1.sandbox().resetBehavior(); - -// expect(sbx1.routes.length).toEqual(1); -// expect(sbx2.routes.length).toEqual(1); - -// sbx2.resetBehavior({ sticky: true }); - -// expect(sbx1.routes.length).toEqual(1); -// expect(sbx2.routes.length).toEqual(0); -// }); -// }); - -// import { -// afterEach, -// beforeEach, -// describe, -// expect, -// it, -// beforeAll, -// vi, -// } from 'vitest'; - -// const { fetchMock } = testGlobals; -// describe('Set up and tear down', () => { -// let fm; -// beforeAll(() => { -// fm = fetchMock.createInstance(); -// fm.config.warnOnUnmatched = false; -// }); -// afterEach(() => fm.restore()); - -// const testChainableMethod = (method, ...args) => { -// it(`${method}() is chainable`, () => { -// expect(fm[method](...args)).toEqual(fm); -// }); - -// it(`${method}() has "this"`, () => { -// vi.spyOn(fm, method).mockReturnThis(); -// expect(fm[method](...args)).toBe(fm); -// fm[method].mockRestore(); -// }); -// }; - -// describe('mock', () => { -// testChainableMethod('mock', '*', 200); - -// it('can be called multiple times', () => { -// expect(() => { -// fm.mock('http://a.com', 200).mock('http://b.com', 200); -// }).not.toThrow(); -// }); - -// it('can be called after fetchMock is restored', () => { -// expect(() => { -// fm.mock('*', 200).restore().mock('*', 200); -// }).not.toThrow(); -// }); - -// describe('parameters', () => { -// beforeEach(() => { -// vi.spyOn(fm, 'compileRoute'); -// vi.spyOn(fm, '_mock').mockReturnValue(fm); -// }); - -// afterEach(() => { -// fm.compileRoute.mockRestore(); -// fm._mock.mockRestore(); -// }); - -// it('accepts single config object', () => { -// const config = { -// url: '*', -// response: 200, -// }; -// expect(() => fm.mock(config)).not.toThrow(); -// expect(fm.compileRoute).toHaveBeenCalledWith([config]); -// expect(fm._mock).toHaveBeenCalled(); -// }); - -// it('accepts matcher, route pairs', () => { -// expect(() => fm.mock('*', 200)).not.toThrow(); -// expect(fm.compileRoute).toHaveBeenCalledWith(['*', 200]); -// expect(fm._mock).toHaveBeenCalled(); -// }); - -// it('accepts matcher, response, config triples', () => { -// expect(() => -// fm.mock('*', 'ok', { -// method: 'PUT', -// some: 'prop', -// }), -// ).not.toThrow(); -// expect(fm.compileRoute).toHaveBeenCalledWith([ -// '*', -// 'ok', -// { -// method: 'PUT', -// some: 'prop', -// }, -// ]); -// expect(fm._mock).toHaveBeenCalled(); -// }); - -// it('expects a matcher', () => { -// expect(() => fm.mock(null, 'ok')).toThrow(); -// }); - -// it('expects a response', () => { -// expect(() => fm.mock('*')).toThrow(); -// }); - -// it('can be called with no parameters', () => { -// expect(() => fm.mock()).not.toThrow(); -// expect(fm.compileRoute).not.toHaveBeenCalled(); -// expect(fm._mock).toHaveBeenCalled(); -// }); - -// it('should accept object responses when also passing options', () => { -// expect(() => -// fm.mock('*', { foo: 'bar' }, { method: 'GET' }), -// ).not.toThrow(); -// }); -// }); -// }); - -// describe('reset', () => { -// testChainableMethod('reset'); - -// it('can be called even if no mocks set', () => { -// expect(() => fm.restore()).not.toThrow(); -// }); - -// it('calls resetHistory', () => { -// vi.spyOn(fm, 'resetHistory'); -// fm.restore(); -// expect(fm.resetHistory).toHaveBeenCalledTimes(1); -// fm.resetHistory.mockRestore(); -// }); - -// it('removes all routing', () => { -// fm.mock('*', 200).catch(200); - -// expect(fm.routes.length).toEqual(1); -// expect(fm.fallbackResponse).toBeDefined(); - -// fm.restore(); - -// expect(fm.routes.length).toEqual(0); -// expect(fm.fallbackResponse).toBeUndefined(); -// }); - -// it('restore is an alias for reset', () => { -// expect(fm.restore).toEqual(fm.reset); -// }); -// }); - - -// describe('spy', () => { -// testChainableMethod('spy'); - -// it('calls catch()', () => { -// vi.spyOn(fm, 'catch'); -// fm.spy(); -// expect(fm.catch).toHaveBeenCalledTimes(1); -// fm.catch.mockRestore(); -// }); -// }); -// }); - - -// import { describe, expect, it, vi } from 'vitest'; - -// const { fetchMock } = testGlobals; -// describe('spy()', () => { -// it('when mocking globally, spy falls through to global fetch', async () => { -// const originalFetch = globalThis.fetch; -// const fetchSpy = vi.fn().mockResolvedValue('example'); - -// globalThis.fetch = fetchSpy; - -// fetchMock.spy(); - -// await globalThis.fetch('http://a.com/', { method: 'get' }); -// expect(fetchSpy).toHaveBeenCalledWith( -// 'http://a.com/', -// { method: 'get' }, -// undefined, -// ); -// fetchMock.restore(); -// globalThis.fetch = originalFetch; -// }); - -// it('when mocking locally, spy falls through to configured fetch', async () => { -// const fetchSpy = vi.fn().mockResolvedValue('dummy'); - -// const fm = fetchMock.sandbox(); -// fm.config.fetch = fetchSpy; - -// fm.spy(); -// await fm.fetchHandler('http://a.com/', { method: 'get' }); -// expect(fetchSpy).toHaveBeenCalledWith( -// 'http://a.com/', -// { method: 'get' }, -// undefined, -// ); -// fm.restore(); -// }); - -// it('can restrict spying to a route', async () => { -// const fetchSpy = vi.fn().mockResolvedValue('dummy'); - -// const fm = fetchMock.sandbox(); -// fm.config.fetch = fetchSpy; - -// fm.spy({ url: 'http://a.com/', method: 'get' }); -// await fm.fetchHandler('http://a.com/', { method: 'get' }); -// expect(fetchSpy).toHaveBeenCalledWith( -// 'http://a.com/', -// { method: 'get' }, -// undefined, -// ); - -// expect(() => fm.fetchHandler('http://b.com/', { method: 'get' })).toThrow(); -// expect(() => -// fm.fetchHandler('http://a.com/', { method: 'post' }), -// ).toThrow(); -// fm.restore(); -// }); -// }); - - -// it('error if spy() is called and no fetch defined in config', () => { -// const fm = fetchMock.sandbox(); -// delete fm.config.fetch; -// expect(() => fm.spy()).toThrow(); -// }); - -// it("don't error if spy() is called and fetch defined in config", () => { -// const fm = fetchMock.sandbox(); -// fm.config.fetch = originalFetch; -// expect(() => fm.spy()).not.toThrow(); -// }); - -// it('exports a properly mocked node-fetch module shape', () => { -// // uses node-fetch default require pattern -// const { -// default: fetch, -// Headers, -// Request, -// Response, -// } = fetchMock.sandbox(); - -// expect(fetch.name).toEqual('fetchMockProxy'); -// expect(new Headers()).toBeInstanceOf(fetchMock.config.Headers); -// expect(new Request('http://a.com')).toBeInstanceOf( -// fetchMock.config.Request, -// ); -// expect(new Response()).toBeInstanceOf(fetchMock.config.Response); -// }); - + testChainableMethod('spy'); + testChainableMethod('spyGlobal'); + it('passes all requests through to the network by default', async () => { + vi.spyOn(fm.config, 'fetch'); + fm.spy(); + try { + await fm.fetchHandler('http://a.com/', { method: 'post' }); + } catch (err) {} + expect(fm.config.fetch).toHaveBeenCalledWith('http://a.com/', { + method: 'post', + }); + fm.config.fetch.mockRestore(); + }); + it('falls through to network for a specific route', async () => { + vi.spyOn(fm.config, 'fetch'); + fm.spy('http://a.com').route('http://b.com', 200); + try { + await fm.fetchHandler('http://a.com/', { method: 'post' }); + await fm.fetchHandler('http://b.com/', { method: 'post' }); + } catch (err) {} + + expect(fm.config.fetch).toHaveBeenCalledTimes(1); + expect(fm.config.fetch).toHaveBeenCalledWith('http://a.com/', { + method: 'post', + }); + fm.config.fetch.mockRestore(); + }); -// // only works in node-fetch@2 -// it.skip('can respond with a readable stream', () => -// new Promise((res) => { -// const readable = new Readable(); -// const write = vi.fn().mockImplementation((chunk, enc, cb) => { -// cb(); -// }); -// const writable = new Writable({ -// write, -// }); -// readable.push('response string'); -// readable.push(null); + it('can apply the full range of matchers and route options', async () => { + vi.spyOn(fm.config, 'fetch'); + fm.spy({ method: 'delete', headers: { check: 'this' } }).catch(); + try { + await fm.fetchHandler('http://a.com/'); + await fm.fetchHandler('http://a.com/', { + method: 'delete', + headers: { check: 'this' }, + }); + } catch (err) {} + expect(fm.config.fetch).toHaveBeenCalledTimes(1); + expect(fm.config.fetch).toHaveBeenCalledWith('http://a.com/', { + method: 'delete', + headers: { check: 'this' }, + }); + fm.config.fetch.mockRestore(); + }); -// fetchMock.route(/a/, readable, { sendAsJson: false }); -// fetchMock.fetchHandler('http://a.com').then((res) => { -// res.body.pipe(writable); -// }); + it('can name a route', async () => { + fm.spy('http://a.com/', 'myroute').catch(); + try { + await fm.fetchHandler('http://a.com/'); + } catch (err) {} + expect(fm.callHistory.called('myroute')).toBe(true); + }); -// writable.on('finish', () => { -// expect(write.args[0][0].toString('utf8')).to.equal('response string'); -// res(); -// }); -// })); + it('plays nice with mockGlobal()', async () => { + globalThis.fetch = fm.config.fetch = vi.fn(); + fm.mockGlobal().spy('http://a.com', 200); + try { + await fm.fetchHandler('http://a.com/', { method: 'post' }); + } catch (err) {} + expect(fm.config.fetch).toHaveBeenCalledTimes(1); + expect(fm.config.fetch).toHaveBeenCalledWith('http://a.com/', { + method: 'post', + }); + }); -// // See https://github.com/wheresrhys/fetch-mock/issues/575 -// it('can respond with large bodies from the interweb', async () => { -// const fm = fetchMock.sandbox(); -// fm.config.fallbackToNetwork = true; -// fm.route(); -// // this is an adequate test because the response hangs if the -// // bug referenced above creeps back in -// await fm -// .fetchHandler('http://www.wheresrhys.co.uk/assets/img/chaffinch.jpg') -// .then((res) => res.blob()); -// }); + it('has spyGlobal() shorthand', async () => { + globalThis.fetch = fm.config.fetch = vi.fn(); + fm.spyGlobal(); + try { + await fm.fetchHandler('http://a.com/', { method: 'post' }); + } catch (err) {} + expect(fm.config.fetch).toHaveBeenCalledTimes(1); + expect(fm.config.fetch).toHaveBeenCalledWith('http://a.com/', { + method: 'post', + }); + }); + }); +}); diff --git a/packages/core/src/__tests__/router-integration.test.js b/packages/core/src/__tests__/router-integration.test.js index 3b4a63e6a..8c741b6c5 100644 --- a/packages/core/src/__tests__/router-integration.test.js +++ b/packages/core/src/__tests__/router-integration.test.js @@ -181,4 +181,85 @@ describe('Router', () => { expect(response.status).toEqual(200); }); }); + describe.skip('random integration tests', () => { + // describe.skip('client-side only tests', () => { + // it('not throw when passing unmatched calls through to native fetch', () => { + // fetchMock.config.fallbackToNetwork = true; + // fetchMock.route(); + // expect(() => fetch('http://a.com')).not.to.throw(); + // fetchMock.config.fallbackToNetwork = false; + // }); + // // this is because we read the body once when normalising the request and + // // want to make sure fetch can still use the sullied request + // it.skip('can send a body on a Request instance when spying ', async () => { + // fetchMock.spy(); + // const req = new fetchMock.config.Request('http://example.com', { + // method: 'post', + // body: JSON.stringify({ prop: 'val' }), + // }); + // try { + // await fetch(req); + // } catch (err) { + // console.log(err); + // expect.unreachable('Fetch should not throw or reject'); + // } + // }); + // 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(); + // }); + // if (globalThis.navigator?.serviceWorker) { + // it('should work within a service worker', async () => { + // const registration = + // await globalThis.navigator.serviceWorker.register('__sw.js'); + // await new Promise((resolve, reject) => { + // if (registration.installing) { + // registration.installing.onstatechange = function () { + // if (this.state === 'activated') { + // resolve(); + // } + // }; + // } else { + // reject('No idea what happened'); + // } + // }); + // await registration.unregister(); + // }); + // } + // // only works in node-fetch@2 + // it.skip('can respond with a readable stream', () => + // new Promise((res) => { + // const readable = new Readable(); + // const write = vi.fn().mockImplementation((chunk, enc, cb) => { + // cb(); + // }); + // const writable = new Writable({ + // write, + // }); + // readable.push('response string'); + // readable.push(null); + // fetchMock.route(/a/, readable, { sendAsJson: false }); + // fetchMock.fetchHandler('http://a.com').then((res) => { + // res.body.pipe(writable); + // }); + // writable.on('finish', () => { + // expect(write.args[0][0].toString('utf8')).to.equal('response string'); + // res(); + // }); + // })); + // // See http://github.com/wheresrhys/fetch-mock/issues/575 + // it('can respond with large bodies from the interweb', async () => { + // const fm = fetchMock.sandbox(); + // fm.config.fallbackToNetwork = true; + // fm.route(); + // // this is an adequate test because the response hangs if the + // // bug referenced above creeps back in + // await fm + // .fetchHandler('http://www.wheresrhys.co.uk/assets/img/chaffinch.jpg') + // .then((res) => res.blob()); + // }); + }); }); diff --git a/packages/core/types/FetchMock.d.ts b/packages/core/types/FetchMock.d.ts index 8312a5add..e86d46a8a 100644 --- a/packages/core/types/FetchMock.d.ts +++ b/packages/core/types/FetchMock.d.ts @@ -99,7 +99,7 @@ import CallHistory from './CallHistory.js'; declare const fetchMock: FetchMockStandalone; declare class FetchMockStandalone extends FetchMock { mockGlobal(this: FetchMockStandalone): FetchMockStandalone; - restoreGlobal(this: FetchMockStandalone): FetchMockStandalone; + unmockGlobal(this: FetchMockStandalone): FetchMockStandalone; spy(this: FetchMockStandalone, matcher?: RouteMatcher | UserRouteConfig, name?: RouteName): FetchMockStandalone; createInstance(): FetchMockStandalone; #private; From 3915bfdff381da15ed93a76d32e8e9f4e4c0becf Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Fri, 26 Jul 2024 11:48:36 +0100 Subject: [PATCH 8/9] build: fix build of all packages --- package.json | 2 +- packages/core/types/FetchMock.d.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e2863e12e..99b08d6b9 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "types:check": "tsc --project ./jsconfig.json && echo 'types check done'", "types:lint": "dtslint --expectOnly packages/fetch-mock/types", "prepare": "husky || echo \"husky not available\"", - "build": "npm run build -w=packages/*", + "build": "npm run build -w=packages", "docs": "npm run start -w docs", "test:ci": "vitest .", "test:legacy": "vitest ./packages/fetch-mock/test/specs", diff --git a/packages/core/types/FetchMock.d.ts b/packages/core/types/FetchMock.d.ts index e86d46a8a..8d1e0287f 100644 --- a/packages/core/types/FetchMock.d.ts +++ b/packages/core/types/FetchMock.d.ts @@ -88,7 +88,7 @@ export type FetchMockGlobalConfig = { matchPartialBody?: boolean; }; export type FetchImplementations = { - fetch?: (arg0: string | Request, arg1: RequestInit) => Promise; + fetch?: typeof fetch; Headers?: typeof Headers; Request?: typeof Request; Response?: typeof Response; @@ -101,6 +101,7 @@ declare class FetchMockStandalone extends FetchMock { mockGlobal(this: FetchMockStandalone): FetchMockStandalone; unmockGlobal(this: FetchMockStandalone): FetchMockStandalone; spy(this: FetchMockStandalone, matcher?: RouteMatcher | UserRouteConfig, name?: RouteName): FetchMockStandalone; + spyGlobal(this: FetchMockStandalone): FetchMockStandalone; createInstance(): FetchMockStandalone; #private; } From 11fca220c5e8dfd5ee46d68c90fd2455353e56c7 Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Fri, 26 Jul 2024 11:50:35 +0100 Subject: [PATCH 9/9] test: catch stray error --- packages/core/src/__tests__/FetchMock/mock-and-spy.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 2793960ff..06ac1746e 100644 --- a/packages/core/src/__tests__/FetchMock/mock-and-spy.test.js +++ b/packages/core/src/__tests__/FetchMock/mock-and-spy.test.js @@ -27,10 +27,12 @@ describe('mock and spy', () => { testChainableMethod('mockGlobal'); testChainableMethod('unmockGlobal'); - it('replaces global fetch with fetchMock.fetchHandler', () => { + it('replaces global fetch with fetchMock.fetchHandler', async () => { vi.spyOn(fm, 'fetchHandler'); fm.mockGlobal(); - fetch('http://a.com', { method: 'post' }); + try { + await fetch('http://a.com', { method: 'post' }); + } catch (err) {} // cannot just check globalThis.fetch === fm.fetchHandler because we apply .bind() to fetchHandler expect(fm.fetchHandler).toHaveBeenCalledWith('http://a.com', { method: 'post',