Skip to content

Commit 83a3f16

Browse files
committed
feat(MockHandler): Allows (HandlerArguments) => JSONValue as MockHandler
fix #1
1 parent 89e4e4b commit 83a3f16

File tree

6 files changed

+918
-120
lines changed

6 files changed

+918
-120
lines changed

src/internal-utils.ts

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,41 @@
11
import * as queryString from 'query-string';
2-
import { HttpMethod, MatcherUrl, RequestUrl } from './types';
2+
import {
3+
HandlerArgument,
4+
HttpMethod,
5+
MatcherUrl,
6+
MockHandler,
7+
MockHandlerFunction,
8+
RequestUrl,
9+
ResponseData
10+
} from './types';
311
import pathToRegex, { Key } from 'path-to-regexp';
12+
import ResponseUtils from './response-utils';
413

5-
export function findRequestUrl(
6-
input: RequestInfo,
7-
init?: RequestInit
8-
): RequestUrl {
14+
export function findRequestUrl(input: RequestInfo, init?: RequestInit): RequestUrl {
915
if (typeof input === 'string') {
1016
return input as RequestUrl;
1117
} else {
1218
return input.url as RequestUrl;
1319
}
1420
}
1521

16-
export function findRequestMethod(
17-
input: RequestInfo,
18-
init?: RequestInit
19-
): HttpMethod {
22+
export function findRequestMethod(input: RequestInfo, init?: RequestInit): HttpMethod {
2023
if (typeof input === 'string') {
2124
return ((init && init.method) || 'GET') as HttpMethod;
2225
} else {
2326
return input.method as HttpMethod;
2427
}
2528
}
2629

27-
export function findPathParams(
28-
requestUrl: RequestUrl,
29-
matcherUrl?: MatcherUrl
30-
): object {
30+
export function findPathParams(requestUrl: RequestUrl, matcherUrl?: MatcherUrl): object {
3131
if (!matcherUrl) {
3232
return {};
3333
}
3434

35-
const urlWithoutQueryParams: RequestUrl = requestUrl.split(
36-
'?'
37-
)[0] as RequestUrl;
35+
const urlWithoutQueryParams: RequestUrl = requestUrl.split('?')[0] as RequestUrl;
3836
const keys: Key[] = [];
3937
const matcherRegex: RegExp = pathToRegex(matcherUrl, keys);
40-
const match: RegExpExecArray | null = matcherRegex.exec(
41-
urlWithoutQueryParams
42-
);
38+
const match: RegExpExecArray | null = matcherRegex.exec(urlWithoutQueryParams);
4339

4440
const sources = keys.map((key, index) => ({
4541
[key.name]: match && match[index + 1]
@@ -62,3 +58,25 @@ export function findBody(input: RequestInfo, init?: RequestInit) {
6258
return init.body;
6359
}
6460
}
61+
62+
export function testPromise(data: any): boolean {
63+
return Promise.resolve(data) == data; // tslint:disable-line
64+
}
65+
66+
export function toMockHandlerFunction(handler: MockHandler): MockHandlerFunction {
67+
if (typeof handler === 'function') {
68+
return (args: HandlerArgument) =>
69+
new Promise<ResponseData>((resolve, reject) => {
70+
const result = handler(args);
71+
const isPromise = testPromise(result);
72+
if (isPromise) {
73+
resolve(result as Promise<ResponseData>);
74+
} else {
75+
const response: ResponseData = { body: JSON.stringify(result) } as ResponseData;
76+
resolve(response);
77+
}
78+
});
79+
} else {
80+
return ResponseUtils.json(handler);
81+
}
82+
}

src/response-utils.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,37 @@
11
import {
22
HandlerArgument,
3+
JSONValue,
34
MockHandler,
45
MockHandlerFunction,
56
ResponseData
67
} from './types';
8+
import { testPromise } from './internal-utils';
79

8-
function execHandler(
9-
handler: MockHandler,
10-
args: HandlerArgument
11-
): Promise<ResponseData> {
10+
function execHandler(handler: MockHandler, args: HandlerArgument): Promise<ResponseData> {
1211
if (typeof handler === 'function') {
13-
return handler(args);
12+
let result = handler(args);
13+
const isPromise = testPromise(result);
14+
if (isPromise) {
15+
return result as Promise<ResponseData>;
16+
} else {
17+
return Promise.resolve({ body: result } as ResponseData);
18+
}
1419
} else {
1520
return ResponseUtils.jsonPromise(handler);
1621
}
1722
}
1823

19-
function unwrap(
20-
args: HandlerArgument
21-
): (handler: MockHandler) => Promise<ResponseData> {
24+
function unwrap(args: HandlerArgument): (handler: MockHandler) => Promise<ResponseData> {
2225
return (handler: MockHandler) => {
2326
if (typeof handler === 'function') {
24-
return handler(args);
27+
const result = handler(args);
28+
const isPromise = testPromise(result);
29+
30+
if (isPromise) {
31+
return result as Promise<ResponseData>;
32+
}
33+
34+
return Promise.resolve({ body: result } as ResponseData);
2535
} else {
2636
return ResponseUtils.jsonPromise(handler);
2737
}
@@ -34,16 +44,16 @@ function merge(into: ResponseData, data: ResponseData): ResponseData {
3444
status: into.status || data.status,
3545
statusText: into.statusText || data.statusText,
3646
headers: Object.assign({}, data.headers, into.headers)
37-
};
47+
} as ResponseData;
3848
}
3949

4050
export default class ResponseUtils {
41-
static json(json: object): MockHandlerFunction {
51+
static json(json: JSONValue): MockHandlerFunction {
4252
return () => ResponseUtils.jsonPromise(json);
4353
}
4454

4555
static jsonPromise(json: MockHandler): Promise<ResponseData> {
46-
const response: ResponseData = { body: JSON.stringify(json) };
56+
const response: ResponseData = { body: JSON.stringify(json) } as ResponseData;
4757
return Promise.resolve(response);
4858
}
4959

@@ -57,23 +67,23 @@ export default class ResponseUtils {
5767

5868
static statusCode(status: number): MockHandler {
5969
return (args: HandlerArgument) => {
60-
const response: ResponseData = { status };
70+
const response: ResponseData = { status } as ResponseData;
6171
return Promise.resolve(response);
6272
};
6373
}
6474

6575
static statusText(statusText: string): MockHandler {
6676
return (args: HandlerArgument) => {
67-
const response: ResponseData = { statusText };
77+
const response: ResponseData = { statusText } as ResponseData;
6878
return Promise.resolve(response);
6979
};
7080
}
7181

7282
static combine(...handlers: MockHandler[]): MockHandler {
7383
return (args: HandlerArgument) => {
74-
return Promise.all(
75-
handlers.map(unwrap(args))
76-
).then((data: ResponseData[]) => data.reduce(merge, {}));
84+
return Promise.all(handlers.map(unwrap(args))).then((data: ResponseData[]) =>
85+
data.reduce(merge, {} as ResponseData)
86+
);
7787
};
7888
}
7989
}

src/types.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
export type Opaque<K, T> = T & { __TYPE__: K };
22

3+
export type JSONValue = string | number | boolean | JSONObject | JSONArray;
4+
export type JSONObject = { [member: string]: JSONValue };
5+
export interface JSONArray extends Array<JSONValue> {}
6+
37
export type HttpMethod =
48
| 'GET'
59
| 'HEAD'
@@ -34,10 +38,10 @@ export interface ResponseData {
3438

3539
export type MockHandler =
3640
| ((args: HandlerArgument) => Promise<ResponseData>)
37-
| object;
38-
export type MockHandlerFunction = (
39-
args: HandlerArgument
40-
) => Promise<ResponseData>;
41+
| ((args: HandlerArgument) => JSONValue)
42+
| JSONValue;
43+
44+
export type MockHandlerFunction = (args: HandlerArgument) => Promise<ResponseData>;
4145
export type RequestUrl = Opaque<'RequestUrl', string>;
4246
export type MatcherUrl = Opaque<'MatcherUrl', string>;
4347

src/yet-another-fetch-mock.ts

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,18 @@ import {
1313
findPathParams,
1414
findQueryParams,
1515
findRequestMethod,
16-
findRequestUrl
16+
findRequestUrl,
17+
toMockHandlerFunction
1718
} from './internal-utils';
1819
import MatcherUtils from './matcher-utils';
19-
import ResponseUtils from './response-utils';
2020

2121
const defaultConfiguration: Configuration = {
2222
enableFallback: true,
2323
middleware: (request, response) => response
2424
};
2525

2626
class FetchMock {
27-
private realFetch: (
28-
input: RequestInfo,
29-
init?: RequestInit
30-
) => Promise<Response>;
27+
private realFetch: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
3128
private configuration: Configuration;
3229
private routes: Route[];
3330
private scope: GlobalFetch;
@@ -40,9 +37,7 @@ class FetchMock {
4037
this.scope.fetch = this.fetchproxy.bind(this);
4138
}
4239

43-
static configure(
44-
configuration: Partial<Configuration> = defaultConfiguration
45-
): FetchMock {
40+
static configure(configuration: Partial<Configuration> = defaultConfiguration): FetchMock {
4641
return new FetchMock(window, configuration);
4742
}
4843

@@ -67,21 +62,11 @@ class FetchMock {
6762
}
6863

6964
mock(matcher: RouteMatcher, handler: MockHandler) {
70-
if (typeof handler === 'function') {
71-
this.routes.push({ matcher, handler });
72-
} else {
73-
this.routes.push({ matcher, handler: ResponseUtils.json(handler) });
74-
}
65+
this.routes.push({ matcher, handler: toMockHandlerFunction(handler) });
7566
}
7667

77-
private fetchproxy(
78-
input: RequestInfo,
79-
init?: RequestInit
80-
): Promise<Response> {
81-
const matchingRoute: Route | undefined = this.findMatchingRoute(
82-
input,
83-
init
84-
);
68+
private fetchproxy(input: RequestInfo, init?: RequestInit): Promise<Response> {
69+
const matchingRoute: Route | undefined = this.findMatchingRoute(input, init);
8570
const url: RequestUrl = findRequestUrl(input, init);
8671
const method: HttpMethod = findRequestMethod(input, init);
8772
const queryParams = findQueryParams(url);
@@ -121,10 +106,7 @@ class FetchMock {
121106
);
122107
}
123108

124-
private findMatchingRoute(
125-
input: RequestInfo,
126-
init?: RequestInit
127-
): Route | undefined {
109+
private findMatchingRoute(input: RequestInfo, init?: RequestInit): Route | undefined {
128110
return this.routes.find((route: Route) => {
129111
return route.matcher.test(input, init);
130112
});

test/yet-another-fetch-mock.test.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,9 @@ describe('FetchMock', () => {
8585
mock.post('/post', { key: 'post' });
8686
mock.delete('/delete', { key: 'delete' });
8787
mock.put('/put', { key: 'put' });
88-
mock.mock(
89-
MatcherUtils.combine(
90-
MatcherUtils.method('HEAD'),
91-
MatcherUtils.url('/head')
92-
),
93-
{
94-
key: 'head'
95-
}
96-
);
88+
mock.mock(MatcherUtils.combine(MatcherUtils.method('HEAD'), MatcherUtils.url('/head')), {
89+
key: 'head'
90+
});
9791

9892
const postReq = fetchToJson('/post', { method: 'POST' }).then(json =>
9993
expect(json.key).toBe('post')
@@ -143,9 +137,7 @@ describe('FetchMock', () => {
143137
it('should support fallback to realFetch', done => {
144138
mock.get('/testurl', { key: 'testurl' });
145139

146-
const mocked = fetchToJson('/testurl').then(json =>
147-
expect(json.key).toBe('testurl')
148-
);
140+
const mocked = fetchToJson('/testurl').then(json => expect(json.key).toBe('testurl'));
149141
const fallback = fetchToJson('https://xkcd.com/info.0.json').then(json =>
150142
expect(json.num).toBeDefined()
151143
);
@@ -155,10 +147,7 @@ describe('FetchMock', () => {
155147

156148
it('should support delayed responses', done => {
157149
mock.get('/test', ResponseUtils.delayed(200, { key: 'delayed' }));
158-
mock.get(
159-
'/test2',
160-
ResponseUtils.delayed(200, ResponseUtils.json({ key: 'delayed2' }))
161-
);
150+
mock.get('/test2', ResponseUtils.delayed(200, ResponseUtils.json({ key: 'delayed2' })));
162151
const startTime = new Date().getTime();
163152

164153
Promise.all([fetchToJson('/test'), fetchToJson('/test2')]).then(json => {
@@ -197,10 +186,7 @@ describe('FetchMock', () => {
197186
it('should be able to combine response utils', done => {
198187
mock.get(
199188
'/combine',
200-
ResponseUtils.combine(
201-
ResponseUtils.json({ key: 'value' }),
202-
ResponseUtils.statusCode(201)
203-
)
189+
ResponseUtils.combine(ResponseUtils.json({ key: 'value' }), ResponseUtils.statusCode(201))
204190
);
205191

206192
mock.get(
@@ -242,4 +228,15 @@ describe('FetchMock', () => {
242228
done();
243229
});
244230
});
231+
232+
it('should jsonValue as response in MockHandler', done => {
233+
const myResponse = ({ queryParams }: HandlerArgument) => ({ key: 'BIG-CASE' });
234+
235+
mock.post('/lowercase', myResponse);
236+
237+
fetchToJson('/lowercase', { method: 'post' }).then(json => {
238+
expect(json.key).toBe('BIG-CASE');
239+
done();
240+
});
241+
});
245242
});

0 commit comments

Comments
 (0)