Skip to content

Commit 88950d2

Browse files
authored
Merge pull request #903 from wheresrhys/rhys/modifyRoute
Rhys/modify route
2 parents 0a7194a + 92e6e36 commit 88950d2

File tree

13 files changed

+4673
-2410
lines changed

13 files changed

+4673
-2410
lines changed

docs/docs/API/more-routing-methods.md

+8
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ fetchMock.purge = function (matcher, response, options) {
6565

6666
Creates a route that only responds to a single request using a particular http method
6767

68+
## modifyRoute(routeName, options)
69+
70+
Modifies a route's behaviour, overwriting any options (including matcher and response) passed into the named route when first created. Useful when writing tests for special cases that require different behaviour to that required by the majority of your tests. To remove an option, pass in null in the options e.g. `.modifyRoute('my-name', {headers: null})`.
71+
72+
## removeRoute(routeName)
73+
74+
Removes a route. Useful when writing tests for special cases that do not require a route that's required by the majority of your tests.
75+
6876
## .addMatcher(options)
6977

7078
Allows adding your own, reusable custom matchers to fetch-mock, for example a matcher for interacting with GraphQL queries, or an `isAuthorized` matcher that encapsulates the exact authorization conditions for the API you are mocking, and only requires a `true` or `false` to be input

docs/docs/Usage/upgrade-guide.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ The same is true for `postOnce()`, `deleteOnce()` etc.
4343

4444
### Options removed
4545

46-
- `overwriteRoutes` - this reflects that multiple routes using the same underlying matcher but different options no longer throw an error.
46+
- `overwriteRoutes` - this reflects that multiple routes using the same underlying matcher but different options no longer throw an error. If you still need to overwrite route behaviour (equivalent to `overwriteRoutes: true`) use [`modifyRoute()` or `removeRoute()`](/fetch-mock/docs/API/more-routing-methods#)
4747
- `warnOnFallback` - given the improved state of node.js debugging tools compared to when fetch-mock was first written, this debugging utilty has been removed.
4848
- `sendAsJson` - fetch-mock@12 implements streams more robustly than previous options, so the user no longer needs to flag when an object response should be converted to JSON.
4949
- `fallbackToNetwork` - The [`spyGlobal()` method](/fetch-mock/docs/API/mocking-and-spying#spyglobal) should now be used.

package-lock.json

+4,397-2,376
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@
4949
"@types/eslint__js": "^8.42.3",
5050
"@types/events": "^3.0.3",
5151
"@types/node": "^20.14.10",
52-
"@vitest/browser": "^2.0.5",
53-
"@vitest/coverage-istanbul": "^2.0.5",
54-
"@vitest/coverage-v8": "^2.0.5",
55-
"@vitest/ui": "^2.0.5",
52+
"@vitest/browser": "^3.0.0",
53+
"@vitest/coverage-istanbul": "^3.0.0",
54+
"@vitest/coverage-v8": "^3.0.0",
55+
"@vitest/ui": "^3.0.0",
5656
"eslint": "9.14.0",
5757
"eslint-config-prettier": "^9.1.0",
5858
"eslint-plugin-prettier": "^5.2.1",
@@ -72,7 +72,7 @@
7272
"typescript": "^5.5.4",
7373
"typescript-eslint": "^8.0.1",
7474
"v8": "^0.1.0",
75-
"vitest": "^2.0.0",
75+
"vitest": "^3.0.0",
7676
"webdriverio": "^8.27.0"
7777
},
7878
"volta": {

packages/codemods/README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ For everyt variable containing an instance of fetch-mock imported using `require
2727
- Rewrite `.lastUrl()`, `.lastOptions()` and `lastResponse()` to their equivalents in fetch-mock@12
2828
- Adds an informative error whenever `.lastCall()` or `.calls()` are used with advice on how to manually correct these
2929
- Converts `.getOnce()`, `.getAnyOnce()`, `.postOnce()` etc... - which have been removed - to calls to the underlying `.get()` method with additional options passed in.
30-
- Removes uses of the deprecated options `overwriteRoutes`, `warnOnFallback`, `sendAsJson`
30+
- Removes uses of the deprecated options `warnOnFallback` and `sendAsJson`
31+
- Removes uses of the deprecated `overwriteRoutes` option, and adds an informative error with details of how to replace with the `modifyRoute()` method
3132
- Removes uses of the deprecated `fallbackToNetwork` option, and adds an informative error with details of how to replace with the `spyGlobal()` method
3233

3334
## Limitations/Out of scope

packages/codemods/src/__tests__/option-codemods.test.js

+50-19
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,22 @@ function expectCodemodResult(src, expected) {
99
}
1010

1111
describe('codemods operating on options', () => {
12+
const overwriteTrueErrorString =
13+
'throw new Error("`overwriteRoutes: true` option is deprecated. Use the `modifyRoute()` method instead")';
1214
['overwriteRoutes', 'warnOnFallback', 'sendAsJson'].forEach((optionName) => {
1315
describe(optionName, () => {
1416
it('Removes as global option when setting directly as property', () => {
15-
expectCodemodResult(`fetchMock.config.${optionName} = true`, '');
17+
expectCodemodResult(`fetchMock.config.${optionName} = false`, '');
1618
});
1719
it('Removes as global option when using Object.assign', () => {
1820
expectCodemodResult(
19-
`Object.assign(fetchMock.config, {${optionName}: true})`,
21+
`Object.assign(fetchMock.config, {${optionName}: false})`,
2022
'',
2123
);
2224
});
2325
it('Removes as global option when using Object.assign alongside other options', () => {
2426
expectCodemodResult(
25-
`Object.assign(fetchMock.config, {${optionName}: true, other: 'value'})`,
27+
`Object.assign(fetchMock.config, {${optionName}: false, other: 'value'})`,
2628
`Object.assign(fetchMock.config, {
2729
other: 'value'
2830
})`,
@@ -60,7 +62,7 @@ describe('codemods operating on options', () => {
6062
if (methodName === 'getAnyOnce') {
6163
it(`Removes as option on third parameter of ${methodName}()`, () => {
6264
expectCodemodResult(
63-
`fetchMock.getAnyOnce(200, {name: 'rio', ${optionName}: true})`,
65+
`fetchMock.getAnyOnce(200, {name: 'rio', ${optionName}: false})`,
6466
`fetchMock.getOnce("*", 200, {
6567
name: 'rio'
6668
})`,
@@ -69,14 +71,14 @@ describe('codemods operating on options', () => {
6971

7072
it(`Removes third parameter of ${methodName}() if no other options remain`, () => {
7173
expectCodemodResult(
72-
`fetchMock.getAnyOnce(200, {${optionName}: true})`,
74+
`fetchMock.getAnyOnce(200, {${optionName}: false})`,
7375
`fetchMock.getOnce("*", 200)`,
7476
);
7577
});
7678
} else if (/any/.test(methodName)) {
7779
it(`Removes as option on third parameter of ${methodName}()`, () => {
7880
expectCodemodResult(
79-
`fetchMock.${methodName}(200, {name: 'rio', ${optionName}: true})`,
81+
`fetchMock.${methodName}(200, {name: 'rio', ${optionName}: false})`,
8082
`fetchMock.${newMethodName}(200, {
8183
name: 'rio'
8284
})`,
@@ -85,14 +87,14 @@ describe('codemods operating on options', () => {
8587

8688
it(`Removes third parameter of ${methodName}() if no other options remain`, () => {
8789
expectCodemodResult(
88-
`fetchMock.${methodName}(200, {${optionName}: true})`,
90+
`fetchMock.${methodName}(200, {${optionName}: false})`,
8991
`fetchMock.${newMethodName}(200)`,
9092
);
9193
});
9294
} else {
9395
it(`Removes as option on first parameter of ${methodName}()`, () => {
9496
expectCodemodResult(
95-
`fetchMock.${methodName}({url: '*', response: 200, ${optionName}: true})`,
97+
`fetchMock.${methodName}({url: '*', response: 200, ${optionName}: false})`,
9698
`fetchMock.${newMethodName}({
9799
url: '*',
98100
response: 200
@@ -101,7 +103,7 @@ describe('codemods operating on options', () => {
101103
});
102104
it(`Removes as option on third parameter of ${methodName}()`, () => {
103105
expectCodemodResult(
104-
`fetchMock.${methodName}('*', 200, {name: 'rio', ${optionName}: true})`,
106+
`fetchMock.${methodName}('*', 200, {name: 'rio', ${optionName}: false})`,
105107
`fetchMock.${newMethodName}('*', 200, {
106108
name: 'rio'
107109
})`,
@@ -110,24 +112,53 @@ describe('codemods operating on options', () => {
110112

111113
it(`Removes third parameter of ${methodName}() if no other options remain`, () => {
112114
expectCodemodResult(
113-
`fetchMock.${methodName}('*', 200, {${optionName}: true})`,
115+
`fetchMock.${methodName}('*', 200, {${optionName}: false})`,
114116
`fetchMock.${newMethodName}('*', 200)`,
115117
);
116118
});
119+
120+
if (optionName === 'overwriteRoutes') {
121+
describe('overwriteRoutes: true', () => {
122+
it(`Removes as option on first parameter of ${methodName}()`, () => {
123+
expectCodemodResult(
124+
`fetchMock.${methodName}({url: '*', response: 200, ${optionName}: true})`,
125+
`fetchMock.${newMethodName}({
126+
url: '*',
127+
response: 200
128+
})${overwriteTrueErrorString}`,
129+
);
130+
});
131+
it(`Removes as option on third parameter of ${methodName}()`, () => {
132+
expectCodemodResult(
133+
`fetchMock.${methodName}('*', 200, {name: 'rio', ${optionName}: true})`,
134+
`fetchMock.${newMethodName}('*', 200, {
135+
name: 'rio'
136+
})${overwriteTrueErrorString}`,
137+
);
138+
});
139+
140+
it(`Removes third parameter of ${methodName}() if no other options remain`, () => {
141+
expectCodemodResult(
142+
`fetchMock.${methodName}('*', 200, {${optionName}: true})`,
143+
`fetchMock.${newMethodName}('*', 200)${overwriteTrueErrorString}`,
144+
);
145+
});
146+
});
147+
}
117148
}
118149
});
119150
});
120151
});
121152
describe('acting on combinations of the 3 options together', () => {
122153
it('Removes as global option when using Object.assign', () => {
123154
expectCodemodResult(
124-
`Object.assign(fetchMock.config, {sendAsJson: true, overwriteRoutes: true})`,
155+
`Object.assign(fetchMock.config, {sendAsJson: true, overwriteRoutes: false})`,
125156
'',
126157
);
127158
});
128159
it('Removes as global option when using Object.assign alongside other options', () => {
129160
expectCodemodResult(
130-
`Object.assign(fetchMock.config, {sendAsJson: true, overwriteRoutes: true, other: 'value'})`,
161+
`Object.assign(fetchMock.config, {sendAsJson: true, overwriteRoutes: false, other: 'value'})`,
131162
`Object.assign(fetchMock.config, {
132163
other: 'value'
133164
})`,
@@ -136,7 +167,7 @@ describe('codemods operating on options', () => {
136167

137168
it(`Removes as option on third parameter of getAnyOnce()`, () => {
138169
expectCodemodResult(
139-
`fetchMock.getAnyOnce(200, {name: 'rio', sendAsJson: true, overwriteRoutes: true})`,
170+
`fetchMock.getAnyOnce(200, {name: 'rio', sendAsJson: true, overwriteRoutes: false})`,
140171
`fetchMock.getOnce("*", 200, {
141172
name: 'rio'
142173
})`,
@@ -145,13 +176,13 @@ describe('codemods operating on options', () => {
145176

146177
it(`Removes third parameter of getAnyOnce() if no other options remain`, () => {
147178
expectCodemodResult(
148-
`fetchMock.getAnyOnce(200, {sendAsJson: true, overwriteRoutes: true})`,
179+
`fetchMock.getAnyOnce(200, {sendAsJson: true, overwriteRoutes: false})`,
149180
`fetchMock.getOnce("*", 200)`,
150181
);
151182
});
152183
it(`Removes as option on third parameter of any()`, () => {
153184
expectCodemodResult(
154-
`fetchMock.any(200, {name: 'rio', sendAsJson: true, overwriteRoutes: true})`,
185+
`fetchMock.any(200, {name: 'rio', sendAsJson: true, overwriteRoutes: false})`,
155186
`fetchMock.any(200, {
156187
name: 'rio'
157188
})`,
@@ -160,13 +191,13 @@ describe('codemods operating on options', () => {
160191

161192
it(`Removes third parameter of any() if no other options remain`, () => {
162193
expectCodemodResult(
163-
`fetchMock.any(200, {sendAsJson: true, overwriteRoutes: true})`,
194+
`fetchMock.any(200, {sendAsJson: true, overwriteRoutes: false})`,
164195
`fetchMock.any(200)`,
165196
);
166197
});
167198
it(`Removes as option on first parameter of get()`, () => {
168199
expectCodemodResult(
169-
`fetchMock.get({url: '*', response: 200, sendAsJson: true, overwriteRoutes: true})`,
200+
`fetchMock.get({url: '*', response: 200, sendAsJson: true, overwriteRoutes: false})`,
170201
`fetchMock.get({
171202
url: '*',
172203
response: 200
@@ -175,7 +206,7 @@ describe('codemods operating on options', () => {
175206
});
176207
it(`Removes as option on third parameter of get()`, () => {
177208
expectCodemodResult(
178-
`fetchMock.get('*', 200, {name: 'rio', sendAsJson: true, overwriteRoutes: true})`,
209+
`fetchMock.get('*', 200, {name: 'rio', sendAsJson: true, overwriteRoutes: false})`,
179210
`fetchMock.get('*', 200, {
180211
name: 'rio'
181212
})`,
@@ -184,7 +215,7 @@ describe('codemods operating on options', () => {
184215

185216
it(`Removes third parameter of get() if no other options remain`, () => {
186217
expectCodemodResult(
187-
`fetchMock.get('*', 200, {sendAsJson: true, overwriteRoutes: true})`,
218+
`fetchMock.get('*', 200, {sendAsJson: true, overwriteRoutes: false})`,
188219
`fetchMock.get('*', 200)`,
189220
);
190221
});

packages/codemods/src/codemods/options.js

+16-5
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,22 @@ module.exports.simpleOptions = function (fetchMockVariableName, root) {
108108
return;
109109
}
110110
simpleOptionNames.forEach((optionName) => {
111-
optionsObjects
112-
.find(j.ObjectProperty, {
113-
key: { name: optionName },
114-
})
115-
.remove();
111+
const properties = optionsObjects.find(j.ObjectProperty, {
112+
key: { name: optionName },
113+
});
114+
115+
properties.forEach((path) => {
116+
if (
117+
path.value.key.name === 'overwriteRoutes' &&
118+
path.value.value.value === true
119+
) {
120+
const errorMessage =
121+
'`overwriteRoutes: true` option is deprecated. Use the `modifyRoute()` method instead';
122+
appendError(errorMessage, j(path));
123+
}
124+
});
125+
126+
properties.remove();
116127
});
117128
optionsObjects
118129
.filter((path) => {

packages/codemods/try.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const { codemod } = require('./src/index.js');
33
console.log(
44
codemod(
55
`const fetchMock = require('fetch-mock');
6-
fetchMock.get('*', 200, {name: 'rio', sendAsJson: true});
6+
fetchMock.get('*', 200, {overwriteRoutes: true});
77
`,
88
),
99
);

packages/fetch-mock/src/FetchMock.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import Router, { RemoveRouteOptions } from './Router.js';
2-
import Route, { RouteName, UserRouteConfig, RouteResponse } from './Route.js';
2+
import Route, {
3+
RouteName,
4+
UserRouteConfig,
5+
RouteResponse,
6+
ModifyRouteConfig,
7+
} from './Route.js';
38
import { MatcherDefinition, RouteMatcher } from './Matchers.js';
49
import CallHistory from './CallHistory.js';
510
import * as requestUtils from './RequestUtils.js';
@@ -126,6 +131,7 @@ export class FetchMock {
126131
this.router.addRoute(matcher, response, options);
127132
return this;
128133
}
134+
129135
catch(response?: RouteResponse): FetchMock {
130136
this.router.setFallback(response);
131137
return this;
@@ -137,6 +143,16 @@ export class FetchMock {
137143
this.router.removeRoutes(options);
138144
return this;
139145
}
146+
removeRoute(routeName: string): FetchMock {
147+
this.router.removeRoutes({ names: [routeName] });
148+
return this;
149+
}
150+
151+
modifyRoute(routeName: string, options: ModifyRouteConfig) {
152+
this.router.modifyRoute(routeName, options);
153+
return this;
154+
}
155+
140156
clearHistory(): FetchMock {
141157
this.callHistory.clear();
142158
return this;

packages/fetch-mock/src/Route.ts

+10
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ export type InternalRouteConfig = {
3838
isFallback?: boolean;
3939
};
4040
export type UserRouteConfig = UserRouteSpecificConfig & FetchMockGlobalConfig;
41+
type Nullable<T> = { [K in keyof T]: T[K] | null };
42+
export type ModifyRouteConfig = Omit<
43+
Nullable<UserRouteSpecificConfig>,
44+
'name' | 'sticky'
45+
>;
46+
4147
export type RouteConfig = UserRouteConfig &
4248
FetchImplementations &
4349
InternalRouteConfig;
@@ -118,6 +124,10 @@ class Route {
118124
matcher: RouteMatcherFunction;
119125

120126
constructor(config: RouteConfig) {
127+
this.init(config);
128+
}
129+
130+
init(config: RouteConfig | ModifyRouteConfig) {
121131
this.config = config;
122132
this.#sanitize();
123133
this.#validate();

packages/fetch-mock/src/Router.ts

+38
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Route, {
66
RouteResponse,
77
RouteResponseData,
88
RouteResponseConfig,
9+
ModifyRouteConfig,
910
} from './Route.js';
1011
import { isUrlMatcher, isFunctionMatcher } from './Matchers.js';
1112
import { RouteMatcher } from './Matchers.js';
@@ -376,4 +377,41 @@ export default class Router {
376377
delete this.fallbackRoute;
377378
}
378379
}
380+
381+
modifyRoute(routeName: string, options: ModifyRouteConfig) {
382+
const route = this.routes.find(
383+
({ config: { name } }) => name === routeName,
384+
);
385+
if (!route) {
386+
throw new Error(
387+
`Cannot call modifyRoute() on route \`${routeName}\`: route of that name not found`,
388+
);
389+
}
390+
if (route.config.sticky) {
391+
throw new Error(
392+
`Cannot call modifyRoute() on route \`${routeName}\`: route is sticky and cannot be modified`,
393+
);
394+
}
395+
396+
if ('name' in options) {
397+
throw new Error(
398+
`Cannot rename the route \`${routeName}\` as \`${options.name}\`: renaming routes is not supported`,
399+
);
400+
}
401+
402+
if ('sticky' in options) {
403+
throw new Error(
404+
`Altering the stickiness of route \`${routeName}\` is not supported`,
405+
);
406+
}
407+
408+
const newConfig = { ...route.config, ...options };
409+
Object.entries(options).forEach(([key, value]) => {
410+
if (value === null) {
411+
// @ts-expect-error this is unsetting a property of user route options, so should be no issue
412+
delete newConfig[key];
413+
}
414+
});
415+
route.init(newConfig);
416+
}
379417
}

0 commit comments

Comments
 (0)