Skip to content

Commit a0a22de

Browse files
authored
Conditional (#128)
* Conditional mocking (#127) * feat: conditional mocking and other enhancements * Add conditional mocking functionality * Add response functions returning string support * Add enableMocks and disableMocks functions * Add optional response function input and init arguments * feat(types): add PR#114 changes * doc(README): fix confusing documentation on conditional mocking * test: correct timeout test * feat: add do/dont mock and add only/never "If" suffix * test: add complex test examples * fix: incorporate changes from review to "once" * fix: type linter errors * Change implementation to use options object * Revert changes for once * Revert readme changes * Remove error log * Cleanup after refactor * Add abort mocking and simplified typescript * Updating readme with changes from API * Cleanup and dependency update Updated all deps to their most recent supported versions. The TypeScript version of the index.d.ts file needed to be updated from 2.3 to 3.0 because Jest 23.x and higher requires TypeScript 3.0. Lowering the Jest version to 22.x (which supports typescript 2.3), causes a Type incompatibility because of a change in the jest.MockInstance generic argument definition. Raising the Jest version to 24.x causes errors as it requires TypeScript 3.1 and higher. Removed the .babelrc file and all babel related dependencies. Babel was only being used for a small amount of ES6 syntax used in the test cases and was complicating maintenance of the dependencies (and greatly increasing the amount of dependencies downloaded). Removal only required 4 lines be changed in the tests. Changed scripts to use yarn instead of npm run. Fix error in abort script * Removing .idea files * Incorporate abort branch * Removing IntelliJ files
1 parent 454fcc9 commit a0a22de

13 files changed

+1705
-4420
lines changed

.babelrc

-3
This file was deleted.

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ node_modules
22
npm-debug.log
33
yarn-error.log
44
coverage
5+
.idea

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
types/test.ts

README.md

+532-36
Large diffs are not rendered by default.

package.json

+24-12
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"main": "src/index.js",
66
"types": "types",
77
"scripts": {
8-
"test": "jest && npm run tsc && npm run dtslint",
8+
"test": "jest && yarn tsc && yarn dtslint",
99
"dtslint": "dtslint types",
1010
"tsc": "tsc"
1111
},
@@ -25,27 +25,39 @@
2525
},
2626
"homepage": "https://github.com/jefflau/jest-fetch-mock#readme",
2727
"dependencies": {
28-
"cross-fetch": "^2.2.2",
29-
"promise-polyfill": "^7.1.1"
28+
"cross-fetch": "^3.0.4",
29+
"promise-polyfill": "^8.1.3"
3030
},
3131
"devDependencies": {
3232
"@types/jest": "^23.3.14",
33-
"@types/node": "^10.12.10",
34-
"babel-core": "^6.26.3",
35-
"babel-jest": "^23.4.2",
36-
"babel-preset-env": "^1.7.0",
37-
"dtslint": "^0.3.0",
38-
"jest": "^23.5.0",
39-
"regenerator-runtime": "^0.12.1",
40-
"typescript": "^3.2.1"
33+
"@types/node": "^10.17.8",
34+
"dtslint": "^2.0.2",
35+
"jest": "^23.6.0",
36+
"prettier": "^1.19.1",
37+
"regenerator-runtime": "^0.13.3",
38+
"typescript": "^3.7.3"
4139
},
4240
"prettier": {
4341
"semi": false,
4442
"editor.formatOnSave": true,
45-
"singleQuote": true
43+
"singleQuote": true,
44+
"overrides": [
45+
{
46+
"files": "**/*.ts",
47+
"options": {
48+
"semi": true,
49+
"tabWidth": 4,
50+
"singleQuote": false,
51+
"printWidth": 120
52+
}
53+
}
54+
]
4655
},
4756
"jest": {
4857
"automock": false,
58+
"testPathIgnorePatterns": [
59+
"types"
60+
],
4961
"setupFiles": [
5062
"./setupJest.js"
5163
]

src/index.js

+194-16
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ if (!Promise) {
1212

1313
const ActualResponse = Response
1414

15-
function ResponseWrapper(body, init) {
15+
function responseWrapper(body, init) {
1616
if (
1717
body &&
1818
typeof body.constructor === 'function' &&
@@ -36,42 +36,220 @@ function ResponseWrapper(body, init) {
3636
return new ActualResponse(body, init)
3737
}
3838

39+
function responseInit(resp, init) {
40+
if (typeof resp.init === 'object') {
41+
return resp.init
42+
} else {
43+
init = Object.assign({}, init || {})
44+
for (const field of ['status', 'statusText', 'headers', 'url']) {
45+
if (field in resp) {
46+
init[field] = resp[field]
47+
}
48+
}
49+
return init
50+
}
51+
}
52+
53+
function requestMatches(urlOrPredicate) {
54+
if (typeof urlOrPredicate === 'function') {
55+
return urlOrPredicate
56+
}
57+
const predicate =
58+
urlOrPredicate instanceof RegExp
59+
? input => urlOrPredicate.exec(input) !== null
60+
: input => input === urlOrPredicate
61+
return input => {
62+
const requestUrl = typeof input === 'object' ? input.url : input
63+
return predicate(requestUrl)
64+
}
65+
}
66+
67+
function requestNotMatches(urlOrPredicate) {
68+
const matches = requestMatches(urlOrPredicate)
69+
return input => {
70+
return !matches(input)
71+
}
72+
}
73+
3974
const isFn = unknown => typeof unknown === 'function'
4075

41-
const normalizeResponse = (bodyOrFunction, init) => () => isFn(bodyOrFunction) ?
42-
bodyOrFunction().then(({body, init}) => new ResponseWrapper(body, init)) :
43-
Promise.resolve(new ResponseWrapper(bodyOrFunction, init))
76+
const isMocking = jest.fn(() => true)
77+
78+
const abortError = () =>
79+
new DOMException('The operation was aborted. ', 'AbortError')
4480

45-
const normalizeError = errorOrFunction => isFn(errorOrFunction) ?
46-
errorOrFunction :
47-
() => Promise.reject(errorOrFunction)
81+
const abort = () => {
82+
throw abortError()
83+
}
84+
85+
const abortAsync = () => {
86+
return Promise.reject(abortError())
87+
}
4888

49-
const fetch = jest.fn()
89+
const normalizeResponse = (bodyOrFunction, init) => (input, reqInit) => {
90+
const request = normalizeRequest(input, reqInit)
91+
return isMocking(input, reqInit)
92+
? isFn(bodyOrFunction)
93+
? bodyOrFunction(request).then(resp => {
94+
if (request.signal && request.signal.aborted) {
95+
abort()
96+
}
97+
return typeof resp === 'string'
98+
? responseWrapper(resp, init)
99+
: responseWrapper(resp.body, responseInit(resp, init))
100+
})
101+
: new Promise((resolve, reject) => {
102+
if (request.signal && request.signal.aborted) {
103+
reject(abortError())
104+
return
105+
}
106+
resolve(responseWrapper(bodyOrFunction, init))
107+
})
108+
: crossFetch.fetch(input, reqInit)
109+
}
110+
111+
const normalizeRequest = (input, reqInit) => {
112+
if (input instanceof Request) {
113+
if (input.signal && input.signal.aborted) {
114+
abort()
115+
}
116+
return input
117+
} else if (typeof input === 'string') {
118+
if (reqInit && reqInit.signal && reqInit.signal.aborted) {
119+
abort()
120+
}
121+
return new Request(input, reqInit)
122+
} else {
123+
throw new TypeError('Unable to parse input as string or Request')
124+
}
125+
}
126+
127+
const normalizeError = errorOrFunction =>
128+
isFn(errorOrFunction)
129+
? errorOrFunction
130+
: () => Promise.reject(errorOrFunction)
131+
132+
const fetch = jest.fn(normalizeResponse(''))
50133
fetch.Headers = Headers
51-
fetch.Response = ResponseWrapper
134+
fetch.Response = responseWrapper
52135
fetch.Request = Request
53-
fetch.mockResponse = (bodyOrFunction, init) => fetch.mockImplementation(normalizeResponse(bodyOrFunction, init))
136+
fetch.mockResponse = (bodyOrFunction, init) =>
137+
fetch.mockImplementation(normalizeResponse(bodyOrFunction, init))
138+
139+
fetch.mockReject = errorOrFunction =>
140+
fetch.mockImplementation(normalizeError(errorOrFunction))
54141

55-
fetch.mockReject = errorOrFunction => fetch.mockImplementation(normalizeError(errorOrFunction))
142+
fetch.mockAbort = () => fetch.mockImplementation(abortAsync)
143+
fetch.mockAbortOnce = () => fetch.mockImplementationOnce(abortAsync)
56144

57-
const mockResponseOnce = (bodyOrFunction, init) => fetch.mockImplementationOnce(normalizeResponse(bodyOrFunction, init))
145+
const mockResponseOnce = (bodyOrFunction, init) =>
146+
fetch.mockImplementationOnce(normalizeResponse(bodyOrFunction, init))
58147

59148
fetch.mockResponseOnce = mockResponseOnce
60149

61150
fetch.once = mockResponseOnce
62151

63-
fetch.mockRejectOnce = errorOrFunction => fetch.mockImplementationOnce(normalizeError(errorOrFunction))
152+
fetch.mockRejectOnce = errorOrFunction =>
153+
fetch.mockImplementationOnce(normalizeError(errorOrFunction))
64154

65155
fetch.mockResponses = (...responses) => {
66-
responses.forEach(([bodyOrFunction, init]) => fetch.mockImplementationOnce(normalizeResponse(bodyOrFunction, init)))
156+
responses.forEach(response => {
157+
if (Array.isArray(response)) {
158+
const [body, init] = response
159+
fetch.mockImplementationOnce(normalizeResponse(body, init))
160+
} else {
161+
fetch.mockImplementationOnce(normalizeResponse(response))
162+
}
163+
})
164+
return fetch
165+
}
166+
167+
fetch.isMocking = isMocking
168+
169+
fetch.mockIf = (urlOrPredicate, bodyOrFunction, init) => {
170+
isMocking.mockImplementation(requestMatches(urlOrPredicate))
171+
if (bodyOrFunction) {
172+
fetch.mockResponse(bodyOrFunction, init)
173+
}
174+
return fetch
175+
}
176+
177+
fetch.dontMockIf = (urlOrPredicate, bodyOrFunction, init) => {
178+
isMocking.mockImplementation(requestNotMatches(urlOrPredicate))
179+
if (bodyOrFunction) {
180+
fetch.mockResponse(bodyOrFunction, init)
181+
}
182+
return fetch
183+
}
184+
185+
fetch.mockOnceIf = (urlOrPredicate, bodyOrFunction, init) => {
186+
isMocking.mockImplementationOnce(requestMatches(urlOrPredicate))
187+
if (bodyOrFunction) {
188+
mockResponseOnce(bodyOrFunction, init)
189+
}
190+
return fetch
191+
}
192+
193+
fetch.dontMockOnceIf = (urlOrPredicate, bodyOrFunction, init) => {
194+
isMocking.mockImplementationOnce(requestNotMatches(urlOrPredicate))
195+
if (bodyOrFunction) {
196+
mockResponseOnce(bodyOrFunction, init)
197+
}
198+
return fetch
199+
}
200+
201+
fetch.dontMock = () => {
202+
isMocking.mockImplementation(() => false)
203+
return fetch
204+
}
205+
206+
fetch.dontMockOnce = () => {
207+
isMocking.mockImplementationOnce(() => false)
208+
return fetch
209+
}
210+
211+
fetch.doMock = (bodyOrFunction, init) => {
212+
isMocking.mockImplementation(() => true)
213+
if (bodyOrFunction) {
214+
fetch.mockResponse(bodyOrFunction, init)
215+
}
216+
return fetch
217+
}
218+
219+
fetch.doMockOnce = (bodyOrFunction, init) => {
220+
isMocking.mockImplementationOnce(() => true)
221+
if (bodyOrFunction) {
222+
mockResponseOnce(bodyOrFunction, init)
223+
}
67224
return fetch
68225
}
69226

70227
fetch.resetMocks = () => {
71228
fetch.mockReset()
229+
isMocking.mockReset()
230+
231+
// reset to default implementation with each reset
232+
fetch.mockImplementation(normalizeResponse(''))
233+
fetch.doMock()
234+
fetch.isMocking = isMocking
72235
}
73236

74-
// Default mock is just a empty string.
75-
fetch.mockResponse('')
237+
fetch.enableMocks = () => {
238+
global.fetchMock = global.fetch = fetch
239+
try {
240+
jest.setMock('node-fetch', fetch)
241+
} catch (error) {
242+
//ignore
243+
}
244+
}
245+
246+
fetch.disableMocks = () => {
247+
global.fetch = crossFetch
248+
try {
249+
jest.dontMock('node-fetch')
250+
} catch (error) {
251+
//ignore
252+
}
253+
}
76254

77255
module.exports = fetch

tests/api.js

+14-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import 'cross-fetch/polyfill'
1+
require('cross-fetch/polyfill')
22

3-
export async function APIRequest(who) {
3+
async function APIRequest(who) {
44
if (who === 'facebook') {
55
const call1 = fetch('https://facebook.com/someOtherResource').then(res =>
66
res.json()
@@ -14,16 +14,18 @@ export async function APIRequest(who) {
1414
}
1515
}
1616

17-
export function APIRequest2(who) {
17+
function APIRequest2(who) {
1818
if (who === 'google') {
1919
return fetch('https://google.com').then(res => res.json())
2020
} else {
2121
return 'no argument provided'
2222
}
2323
}
2424

25-
export function request() {
26-
return fetch('https://randomuser.me/api', {})
25+
const defaultRequestUri = 'https://randomuser.me/api'
26+
27+
function request(uri = defaultRequestUri) {
28+
return fetch(uri, {})
2729
.then(response => {
2830
const contentType = response.headers.get('content-type')
2931

@@ -46,3 +48,10 @@ export function request() {
4648
throw new Error(errorData.error)
4749
})
4850
}
51+
52+
module.exports = {
53+
request,
54+
APIRequest,
55+
APIRequest2,
56+
defaultRequestUri
57+
}

0 commit comments

Comments
 (0)