Skip to content

Commit f62e3d6

Browse files
committed
feat: implemented modifyRoute() method
1 parent 2398567 commit f62e3d6

File tree

6 files changed

+90
-5
lines changed

6 files changed

+90
-5
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Creates a route that only responds to a single request using a particular http m
6767

6868
## modifyRoute(routeName, options)
6969

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.
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})`.
7171

7272
## removeRoute(routeName)
7373

packages/fetch-mock/src/FetchMock.ts

+11-2
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+
NullableUserRouteConfig,
7+
} from './Route.js';
38
import { MatcherDefinition, RouteMatcher } from './Matchers.js';
49
import CallHistory from './CallHistory.js';
510
import * as requestUtils from './RequestUtils.js';
@@ -138,12 +143,16 @@ export class FetchMock {
138143
this.router.removeRoutes(options);
139144
return this;
140145
}
141-
142146
removeRoute(routeName: string): FetchMock {
143147
this.router.removeRoutes({ names: [routeName] });
144148
return this;
145149
}
146150

151+
modifyRoute(routeName: string, options: NullableUserRouteConfig) {
152+
this.router.modifyRoute(routeName, options);
153+
return this;
154+
}
155+
147156
clearHistory(): FetchMock {
148157
this.callHistory.clear();
149158
return this;

packages/fetch-mock/src/Route.ts

+7
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ 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 NullableUserRouteConfig = Nullable<UserRouteConfig>;
43+
4144
export type RouteConfig = UserRouteConfig &
4245
FetchImplementations &
4346
InternalRouteConfig;
@@ -118,6 +121,10 @@ class Route {
118121
matcher: RouteMatcherFunction;
119122

120123
constructor(config: RouteConfig) {
124+
this.init(config);
125+
}
126+
127+
init(config: RouteConfig | NullableUserRouteConfig) {
121128
this.config = config;
122129
this.#sanitize();
123130
this.#validate();

packages/fetch-mock/src/Router.ts

+39
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,21 @@ import Route, {
66
RouteResponse,
77
RouteResponseData,
88
RouteResponseConfig,
9+
NullableUserRouteConfig,
910
} from './Route.js';
1011
import { isUrlMatcher, isFunctionMatcher } from './Matchers.js';
1112
import { RouteMatcher } from './Matchers.js';
1213
import { FetchMockConfig } from './FetchMock.js';
1314
import { hasCredentialsInUrl } from './RequestUtils.js';
1415
import type { CallLog } from './CallHistory.js';
1516

17+
// const nullablePerson: Nullable<Person> = {
18+
// name: 'Daffodil',
19+
// email: null,
20+
// age: null,
21+
// admin: true,
22+
// };
23+
1624
export type ResponseConfigProp =
1725
| 'body'
1826
| 'headers'
@@ -376,4 +384,35 @@ export default class Router {
376384
delete this.fallbackRoute;
377385
}
378386
}
387+
388+
modifyRoute(routeName: string, options: NullableUserRouteConfig) {
389+
const route = this.routes.find(
390+
({ config: { name } }) => name === routeName,
391+
);
392+
if (!route) {
393+
throw new Error(
394+
`Cannot call modifyRoute() on route \`${routeName}\`: route of that name not found`,
395+
);
396+
}
397+
if (route.config.sticky) {
398+
throw new Error(
399+
`Cannot call modifyRoute() on route \`${routeName}\`: route is sticky and cannot be modified`,
400+
);
401+
}
402+
403+
if (options.name) {
404+
throw new Error(
405+
`Cannot rename the route \`${routeName}\` as \`${options.name}\`: renaming routes is not supported`,
406+
);
407+
}
408+
409+
const newConfig = { ...route.config, ...options };
410+
Object.entries(options).forEach(([key, value]) => {
411+
if (value === null) {
412+
// @ts-expect-error this is unsetting a property of user route options, so should be no issue
413+
delete newConfig[key];
414+
}
415+
});
416+
route.init(newConfig);
417+
}
379418
}

packages/fetch-mock/src/__tests__/FetchMock/mock-and-spy.test.js

+5
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ describe('mock and spy', () => {
141141
const isBrowser = Boolean(globalThis.location);
142142
// avoids getting caught by a cors error
143143
const testUrl = isBrowser ? '/' : 'http://example.com/';
144+
try {
145+
await fm.fetchHandler('testUrl');
146+
} catch (err) {
147+
console.log(err);
148+
}
144149
await expect(fm.fetchHandler(testUrl)).resolves.toBeInstanceOf(Response);
145150
});
146151
});

packages/fetch-mock/src/__tests__/FetchMock/routing.test.js

+27-2
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ describe('Routing', () => {
251251
});
252252
});
253253
describe('modifyRoute', () => {
254+
// testChainableMethod(`modifyRoute`);
254255
it('can modify a matcher', async () => {
255256
fm.route('http://a.com/', 200, 'named');
256257
fm.modifyRoute('named', {
@@ -315,11 +316,35 @@ describe('Routing', () => {
315316
fm.modifyRoute('named', {
316317
headers: null,
317318
}),
318-
).toError('Cannot call modifyRoute() on sticky route `named`');
319+
).toThrow(
320+
'Cannot call modifyRoute() on route `named`: route is sticky and cannot be modified',
321+
);
322+
});
323+
324+
it('errors when route not found', () => {
325+
fm.route('http://a.com/', 200, 'named');
326+
expect(() =>
327+
fm.modifyRoute('wrong name', {
328+
headers: null,
329+
}),
330+
).toThrow(
331+
'Cannot call modifyRoute() on route `wrong name`: route of that name not found',
332+
);
333+
});
334+
335+
it('errors when trying to rename a route', () => {
336+
fm.route('http://a.com/', 200, { name: 'named' });
337+
expect(() =>
338+
fm.modifyRoute('named', {
339+
name: 'new name',
340+
}),
341+
).toThrow(
342+
'Cannot rename the route `named` as `new name`: renaming routes is not supported',
343+
);
319344
});
320345
});
321346
describe('removeRoute', () => {
322-
testChainableRoutingMethod(`removeRoute`);
347+
testChainableMethod(`removeRoute`);
323348
it.skip('error informatively when name not found', () => {
324349
fm.route('http://a.com/', 200).route('http://b.com/', 201, 'named');
325350
expect(() => fm.removeRoute('misnamed')).toThrowError(

0 commit comments

Comments
 (0)