From 41e2475d64d909f5fb686f2fe3709243326f2dba Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Sun, 21 Jul 2024 12:46:46 +0100 Subject: [PATCH] feat: can now access express parameters in responses --- docs/docs/@fetch-mock/core/CallHistory.md | 2 + docs/docs/@fetch-mock/core/route/matcher.md | 4 ++ packages/core/src/Matchers.js | 49 ++++++++++--------- .../src/__tests__/Matchers/express.test.js | 33 +++++++++++++ 4 files changed, 66 insertions(+), 22 deletions(-) diff --git a/docs/docs/@fetch-mock/core/CallHistory.md b/docs/docs/@fetch-mock/core/CallHistory.md index 2037bc01..df4922e0 100644 --- a/docs/docs/@fetch-mock/core/CallHistory.md +++ b/docs/docs/@fetch-mock/core/CallHistory.md @@ -10,11 +10,13 @@ sidebar_position: 4 Calls are recorded, and returned, in a standard format with the following properties: +- `[string|Request,Object]` - the original arguments passed in to `fetch` - `{string} url` - The url being fetched - `{NormalizedRequestOptions} options` - The options passed in to the fetch (may be derived from a `Request` if one was used) - `{Request} [request]` - The `Request` passed to fetch, if one was used - `{Route} [route]` - The route used to handle the request - `{Response} [response]` - The `Response` returned to the user +- `{Object.}` - Any express parameters extracted from the `url` - `{Promise[]} pendingPromises` - An internal structure used by the `.flush()` method documented below ## Filtering diff --git a/docs/docs/@fetch-mock/core/route/matcher.md b/docs/docs/@fetch-mock/core/route/matcher.md index ac7abe04..1e24eec5 100644 --- a/docs/docs/@fetch-mock/core/route/matcher.md +++ b/docs/docs/@fetch-mock/core/route/matcher.md @@ -78,6 +78,10 @@ When the `express:` keyword is used in a string matcher, it can be combined with } ``` +The values of express parameters are made available in the `expressParams` property when +- [Inspecting call history](/fetch-mock/docs/@fetch-mock/core/CallHistory#calllog-schema) +- [Using a function to construct a response](/fetch-mock/docs/@fetch-mock/core/route/response#function) + ## Other matching criteria ### method diff --git a/packages/core/src/Matchers.js b/packages/core/src/Matchers.js index 19427e6c..0a8145d8 100644 --- a/packages/core/src/Matchers.js +++ b/packages/core/src/Matchers.js @@ -61,7 +61,21 @@ const stringMatchers = { }, express: (targetString) => { const urlRX = regexparam.parse(targetString); - return ({ url }) => urlRX.pattern.test(getPath(url)); + return (callLog) => { + const vals = urlRX.pattern.exec(getPath(callLog.url)); + if (!vals) { + callLog.expressParams = {}; + return false; + } + vals.shift(); + /** @type {Object.} */ + callLog.expressParams = urlRX.keys.reduce( + (map, paramName, i) => + vals[i] ? Object.assign(map, { [paramName]: vals[i] }) : map, + {}, + ); + return true; + }; }, path: (targetString) => @@ -124,30 +138,21 @@ const getQueryStringMatcher = ({ query: passedQuery }) => { /** * @type {MatcherGenerator} */ -const getParamsMatcher = ({ params: expectedParams, url: matcherUrl }) => { +const getParamsMatcher = ({ params: expectedParams, url }) => { if (!expectedParams) { return; } - if (typeof matcherUrl === 'string') { - if (!/express:/.test(matcherUrl)) { - throw new Error( - 'fetch-mock: matching on params is only possible when using an express: matcher', - ); - } - const expectedKeys = Object.keys(expectedParams); - const re = regexparam.parse(matcherUrl.replace(/^express:/, '')); - return ({ url }) => { - const vals = re.pattern.exec(getPath(url)) || []; - vals.shift(); - /** @type {Object.} */ - const params = re.keys.reduce( - (map, paramName, i) => - vals[i] ? Object.assign(map, { [paramName]: vals[i] }) : map, - {}, - ); - return expectedKeys.every((key) => params[key] === expectedParams[key]); - }; + if (!(typeof url === 'string' && /express:/.test(url))) { + throw new Error( + 'fetch-mock: matching on params is only possible when using an express: matcher', + ); } + const expectedKeys = Object.keys(expectedParams); + return ({ expressParams = {} }) => { + return expectedKeys.every( + (key) => expressParams[key] === expectedParams[key], + ); + }; }; /** * @type {MatcherGenerator} @@ -238,11 +243,11 @@ const getUrlMatcher = (route) => { /** @type {MatcherDefinition[]} */ export const builtInMatchers = [ + { name: 'url', matcher: getUrlMatcher }, { name: 'query', matcher: getQueryStringMatcher }, { name: 'method', matcher: getMethodMatcher }, { name: 'headers', matcher: getHeaderMatcher }, { name: 'params', matcher: getParamsMatcher }, { name: 'body', matcher: getBodyMatcher, usesBody: true }, { name: 'matcherFunction', matcher: getFunctionMatcher }, - { name: 'url', matcher: getUrlMatcher }, ]; diff --git a/packages/core/src/__tests__/Matchers/express.test.js b/packages/core/src/__tests__/Matchers/express.test.js index 678799e3..27d31e9a 100644 --- a/packages/core/src/__tests__/Matchers/express.test.js +++ b/packages/core/src/__tests__/Matchers/express.test.js @@ -44,4 +44,37 @@ describe('express path parameter matching', () => { true, ); }); + + it('can match based on the existence, not value, of a parameter', () => { + const route = new Route({ + url: 'express:/type/:instance', + response: 200, + }); + expect(route.matcher({ url: '/nottype/a' })).toBe(false); + expect(route.matcher({ url: '/type/a' })).toBe(true); + }); + it('writes parameter values to the callLog', () => { + const route = new Route({ + url: 'express:/type/:instance', + response: 200, + params: { instance: 'b' }, + }); + const callLog = { url: '/type/a' }; + route.matcher(callLog); + expect(callLog.expressParams).toEqual({ instance: 'a' }); + + const callLog2 = { url: '/type/b' }; + route.matcher(callLog2); + expect(callLog2.expressParams).toEqual({ instance: 'b' }); + }); + + it('writes parameter values to the callLog even if not matched on', () => { + const route = new Route({ + url: 'express:/type/:instance', + response: 200, + }); + const callLog = { url: '/type/a' }; + route.matcher(callLog); + expect(callLog.expressParams).toEqual({ instance: 'a' }); + }); });