hungry-fetch
Version:
A hungry fetch mock.
170 lines (142 loc) • 3.85 kB
text/typescript
/**
* This monkeypatches fetch and stores all requests send with fetch.
* fetch calls are simply resolved with no data.
*
* With this you can test what your code is sending via fetch.
*/
import FetchCall from './fetch-call';
import { testWildcardPattern } from './url-pattern';
let fetchRequests: FetchCall[] = [];
let mockResponses: ResponseData[] = [];
export type Body = Record<string, any> | string;
export type ResponseData = {
pattern: string;
body: Record<string, any> | string;
config: {
contentType?: string;
headers?: { [key: string]: string };
status?: number;
};
resolve?: boolean;
};
function getResponse(responseData: ResponseData) {
let body;
let contentType;
if (typeof responseData.body === 'object') {
// json response
body = JSON.stringify(responseData.body);
contentType = 'application/json';
} else {
// plain text
body = responseData.body;
contentType = 'text/plain';
}
const { config } = responseData;
return new Response(body, {
headers: {
'Content-Type': config.contentType || contentType,
...(config.headers || {}),
},
status: config.status || 200,
});
}
function testUrl(url: string, pattern: string) {
// exact match
if (url === pattern) {
return 1.0;
}
// any pattern
if (pattern === '*') {
return 0.1;
}
// match using wildcards
if (testWildcardPattern(url, pattern)) {
return 0.5;
}
// no match
return 0;
}
function getMockResponse(url: string): ResponseData | null {
if (mockResponses.length === 0) return null;
return mockResponses.reduce<ResponseData | null>((acc, cur) => {
const curWeight = testUrl(url, cur.pattern);
if (curWeight === 0) {
return acc;
}
if (acc === null || curWeight > testUrl(url, acc.pattern)) {
return cur;
}
return acc;
}, null);
}
export function clear() {
fetchRequests = [];
mockResponses = [];
}
export function calls() {
return fetchRequests;
}
export function lastCall() {
if (fetchRequests.length === 0) {
throw new Error('fetch(…) has not been called!');
}
return fetchRequests[fetchRequests.length - 1];
}
export function singleCall() {
if (fetchRequests.length !== 1) {
throw new Error(
`fetch(…) not called exactly one time! It was called ${fetchRequests.length} times.`
);
}
return lastCall();
}
/**
* Mock a response with a url matching the specified pattern.
*
* The pattern supports wildcards for parts of the url by using an asterisk (\*).
* Example: /category/*/details
*
* You may also use a single asterisk "*" to match all routes.
*
* The matchers are weighted so that a perfect match has the highest priority.
* A match using wildcard components has the second highest prority. The
* single asterisk matcher has the lowest priority. This helps to configure
* default responses.
*
* @param urlMatcher The matcher incoming urls will be checked against.
* @param body The body of the response.
* @param config An optional config.
* @param resolve Set to false to reject matching fetch calls.
*/
export function mockResponse(
pattern: string,
body: Body,
config = {},
resolve = true
) {
mockResponses.push({
pattern,
body,
resolve,
config,
});
}
(typeof window !== 'undefined' ? window : (global as any)).fetch = (
url: string,
request: RequestInit,
...args: any[]
) =>
new Promise<void | Response>((resolve, reject) => {
fetchRequests.push(new FetchCall(url, request, args));
const mockedResponse = getMockResponse(url);
if (mockedResponse) {
const response = getResponse(mockedResponse);
if (mockedResponse.resolve) {
resolve(response);
} else {
reject(new Error('Failed to fetch'));
}
} else {
resolve();
}
});