1 | const { setDebugPhase, setDebugNamespace, debug } = require('./debug');
|
2 | const { normalizeUrl } = require('./request-utils');
|
3 | const FetchMock = {};
|
4 | const isName = (nameOrMatcher) =>
|
5 | typeof nameOrMatcher === 'string' && /^[\da-zA-Z\-]+$/.test(nameOrMatcher);
|
6 |
|
7 | const filterCallsWithMatcher = function (matcher, options = {}, calls) {
|
8 | matcher = this.generateMatcher(
|
9 | this.sanitizeRoute(Object.assign({ matcher }, options))
|
10 | );
|
11 | return calls.filter(([url, options]) => matcher(normalizeUrl(url), options));
|
12 | };
|
13 |
|
14 | const formatDebug = (func) => {
|
15 | return function (...args) {
|
16 | setDebugPhase('inspect');
|
17 | const result = func.call(this, ...args);
|
18 | setDebugPhase();
|
19 | return result;
|
20 | };
|
21 | };
|
22 |
|
23 | FetchMock.filterCalls = function (nameOrMatcher, options) {
|
24 | debug('Filtering fetch calls');
|
25 | let calls = this._calls;
|
26 | let matcher = '*';
|
27 |
|
28 | if ([true, 'matched'].includes(nameOrMatcher)) {
|
29 | debug(`Filter provided is ${nameOrMatcher}. Returning matched calls only`);
|
30 | calls = calls.filter(({ isUnmatched }) => !isUnmatched);
|
31 | } else if ([false, 'unmatched'].includes(nameOrMatcher)) {
|
32 | debug(
|
33 | `Filter provided is ${nameOrMatcher}. Returning unmatched calls only`
|
34 | );
|
35 | calls = calls.filter(({ isUnmatched }) => isUnmatched);
|
36 | } else if (typeof nameOrMatcher === 'undefined') {
|
37 | debug(`Filter provided is undefined. Returning all calls`);
|
38 | calls = calls;
|
39 | } else if (isName(nameOrMatcher)) {
|
40 | debug(
|
41 | `Filter provided, looks like the name of a named route. Returning only calls handled by that route`
|
42 | );
|
43 | calls = calls.filter(({ identifier }) => identifier === nameOrMatcher);
|
44 | } else {
|
45 | matcher = normalizeUrl(nameOrMatcher);
|
46 | if (this.routes.some(({ identifier }) => identifier === matcher)) {
|
47 | debug(
|
48 | `Filter provided, ${nameOrMatcher}, identifies a route. Returning only calls handled by that route`
|
49 | );
|
50 | calls = calls.filter((call) => call.identifier === matcher);
|
51 | }
|
52 | }
|
53 |
|
54 | if ((options || matcher !== '*') && calls.length) {
|
55 | if (typeof options === 'string') {
|
56 | options = { method: options };
|
57 | }
|
58 | debug(
|
59 | 'Compiling filter and options to route in order to filter all calls',
|
60 | nameOrMatcher
|
61 | );
|
62 | calls = filterCallsWithMatcher.call(this, matcher, options, calls);
|
63 | }
|
64 | debug(`Retrieved ${calls.length} calls`);
|
65 | return calls;
|
66 | };
|
67 |
|
68 | FetchMock.calls = formatDebug(function (nameOrMatcher, options) {
|
69 | debug('retrieving matching calls');
|
70 | return this.filterCalls(nameOrMatcher, options);
|
71 | });
|
72 |
|
73 | FetchMock.lastCall = formatDebug(function (nameOrMatcher, options) {
|
74 | debug('retrieving last matching call');
|
75 | return [...this.filterCalls(nameOrMatcher, options)].pop();
|
76 | });
|
77 |
|
78 | FetchMock.lastUrl = formatDebug(function (nameOrMatcher, options) {
|
79 | debug('retrieving url of last matching call');
|
80 | return (this.lastCall(nameOrMatcher, options) || [])[0];
|
81 | });
|
82 |
|
83 | FetchMock.lastOptions = formatDebug(function (nameOrMatcher, options) {
|
84 | debug('retrieving options of last matching call');
|
85 | return (this.lastCall(nameOrMatcher, options) || [])[1];
|
86 | });
|
87 |
|
88 | FetchMock.called = formatDebug(function (nameOrMatcher, options) {
|
89 | debug('checking if matching call was made');
|
90 | return Boolean(this.filterCalls(nameOrMatcher, options).length);
|
91 | });
|
92 |
|
93 | FetchMock.flush = formatDebug(async function (waitForResponseMethods) {
|
94 | setDebugNamespace('flush');
|
95 | debug(
|
96 | `flushing all fetch calls. ${
|
97 | waitForResponseMethods ? '' : 'Not '
|
98 | }waiting for response bodies to complete download`
|
99 | );
|
100 |
|
101 | const queuedPromises = this._holdingPromises;
|
102 | this._holdingPromises = [];
|
103 | debug(`${queuedPromises.length} fetch calls to be awaited`);
|
104 |
|
105 | await Promise.all(queuedPromises);
|
106 | debug(`All fetch calls have completed`);
|
107 | if (waitForResponseMethods && this._holdingPromises.length) {
|
108 | debug(`Awaiting all fetch bodies to download`);
|
109 | await this.flush(waitForResponseMethods);
|
110 | debug(`All fetch bodies have completed downloading`);
|
111 | }
|
112 | setDebugNamespace();
|
113 | });
|
114 |
|
115 | FetchMock.done = formatDebug(function (nameOrMatcher) {
|
116 | setDebugPhase('inspect');
|
117 | setDebugNamespace('done');
|
118 | debug('Checking to see if expected calls have been made');
|
119 | let routesToCheck;
|
120 |
|
121 | if (nameOrMatcher && typeof nameOrMatcher !== 'boolean') {
|
122 | debug(
|
123 | 'Checking to see if expected calls have been made for single route:',
|
124 | nameOrMatcher
|
125 | );
|
126 | routesToCheck = [{ identifier: nameOrMatcher }];
|
127 | } else {
|
128 | debug('Checking to see if expected calls have been made for all routes');
|
129 | routesToCheck = this.routes;
|
130 | }
|
131 |
|
132 |
|
133 |
|
134 | const result = routesToCheck
|
135 | .map(({ identifier }) => {
|
136 | if (!this.called(identifier)) {
|
137 | debug('No calls made for route:', identifier);
|
138 | console.warn(`Warning: ${identifier} not called`);
|
139 | return false;
|
140 | }
|
141 |
|
142 | const expectedTimes = (
|
143 | this.routes.find((r) => r.identifier === identifier) || {}
|
144 | ).repeat;
|
145 |
|
146 | if (!expectedTimes) {
|
147 | debug(
|
148 | 'Route has been called at least once, and no expectation of more set:',
|
149 | identifier
|
150 | );
|
151 | return true;
|
152 | }
|
153 | const actualTimes = this.filterCalls(identifier).length;
|
154 |
|
155 | debug(`Route called ${actualTimes} times:`, identifier);
|
156 | if (expectedTimes > actualTimes) {
|
157 | debug(
|
158 | `Route called ${actualTimes} times, but expected ${expectedTimes}:`,
|
159 | identifier
|
160 | );
|
161 | console.warn(
|
162 | `Warning: ${identifier} only called ${actualTimes} times, but ${expectedTimes} expected`
|
163 | );
|
164 | return false;
|
165 | } else {
|
166 | return true;
|
167 | }
|
168 | })
|
169 | .every((isDone) => isDone);
|
170 |
|
171 | setDebugNamespace();
|
172 | setDebugPhase();
|
173 | return result;
|
174 | });
|
175 |
|
176 | module.exports = FetchMock;
|