Skip to content

Commit

Permalink
Conditional (#128)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jefflau committed Dec 18, 2019
1 parent 454fcc9 commit a0a22de
Show file tree
Hide file tree
Showing 13 changed files with 1,705 additions and 4,420 deletions.
3 changes: 0 additions & 3 deletions .babelrc

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules
npm-debug.log
yarn-error.log
coverage
.idea
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
types/test.ts
568 changes: 532 additions & 36 deletions README.md

Large diffs are not rendered by default.

36 changes: 24 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "src/index.js",
"types": "types",
"scripts": {
"test": "jest && npm run tsc && npm run dtslint",
"test": "jest && yarn tsc && yarn dtslint",
"dtslint": "dtslint types",
"tsc": "tsc"
},
Expand All @@ -25,27 +25,39 @@
},
"homepage": "https://github.com/jefflau/jest-fetch-mock#readme",
"dependencies": {
"cross-fetch": "^2.2.2",
"promise-polyfill": "^7.1.1"
"cross-fetch": "^3.0.4",
"promise-polyfill": "^8.1.3"
},
"devDependencies": {
"@types/jest": "^23.3.14",
"@types/node": "^10.12.10",
"babel-core": "^6.26.3",
"babel-jest": "^23.4.2",
"babel-preset-env": "^1.7.0",
"dtslint": "^0.3.0",
"jest": "^23.5.0",
"regenerator-runtime": "^0.12.1",
"typescript": "^3.2.1"
"@types/node": "^10.17.8",
"dtslint": "^2.0.2",
"jest": "^23.6.0",
"prettier": "^1.19.1",
"regenerator-runtime": "^0.13.3",
"typescript": "^3.7.3"
},
"prettier": {
"semi": false,
"editor.formatOnSave": true,
"singleQuote": true
"singleQuote": true,
"overrides": [
{
"files": "**/*.ts",
"options": {
"semi": true,
"tabWidth": 4,
"singleQuote": false,
"printWidth": 120
}
}
]
},
"jest": {
"automock": false,
"testPathIgnorePatterns": [
"types"
],
"setupFiles": [
"./setupJest.js"
]
Expand Down
210 changes: 194 additions & 16 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ if (!Promise) {

const ActualResponse = Response

function ResponseWrapper(body, init) {
function responseWrapper(body, init) {
if (
body &&
typeof body.constructor === 'function' &&
Expand All @@ -36,42 +36,220 @@ function ResponseWrapper(body, init) {
return new ActualResponse(body, init)
}

function responseInit(resp, init) {
if (typeof resp.init === 'object') {
return resp.init
} else {
init = Object.assign({}, init || {})
for (const field of ['status', 'statusText', 'headers', 'url']) {
if (field in resp) {
init[field] = resp[field]
}
}
return init
}
}

function requestMatches(urlOrPredicate) {
if (typeof urlOrPredicate === 'function') {
return urlOrPredicate
}
const predicate =
urlOrPredicate instanceof RegExp
? input => urlOrPredicate.exec(input) !== null
: input => input === urlOrPredicate
return input => {
const requestUrl = typeof input === 'object' ? input.url : input
return predicate(requestUrl)
}
}

function requestNotMatches(urlOrPredicate) {
const matches = requestMatches(urlOrPredicate)
return input => {
return !matches(input)
}
}

const isFn = unknown => typeof unknown === 'function'

const normalizeResponse = (bodyOrFunction, init) => () => isFn(bodyOrFunction) ?
bodyOrFunction().then(({body, init}) => new ResponseWrapper(body, init)) :
Promise.resolve(new ResponseWrapper(bodyOrFunction, init))
const isMocking = jest.fn(() => true)

const abortError = () =>
new DOMException('The operation was aborted. ', 'AbortError')

const normalizeError = errorOrFunction => isFn(errorOrFunction) ?
errorOrFunction :
() => Promise.reject(errorOrFunction)
const abort = () => {
throw abortError()
}

const abortAsync = () => {
return Promise.reject(abortError())
}

const fetch = jest.fn()
const normalizeResponse = (bodyOrFunction, init) => (input, reqInit) => {
const request = normalizeRequest(input, reqInit)
return isMocking(input, reqInit)
? isFn(bodyOrFunction)
? bodyOrFunction(request).then(resp => {
if (request.signal && request.signal.aborted) {
abort()
}
return typeof resp === 'string'
? responseWrapper(resp, init)
: responseWrapper(resp.body, responseInit(resp, init))
})
: new Promise((resolve, reject) => {
if (request.signal && request.signal.aborted) {
reject(abortError())
return
}
resolve(responseWrapper(bodyOrFunction, init))
})
: crossFetch.fetch(input, reqInit)
}

const normalizeRequest = (input, reqInit) => {
if (input instanceof Request) {
if (input.signal && input.signal.aborted) {
abort()
}
return input
} else if (typeof input === 'string') {
if (reqInit && reqInit.signal && reqInit.signal.aborted) {
abort()
}
return new Request(input, reqInit)
} else {
throw new TypeError('Unable to parse input as string or Request')
}
}

const normalizeError = errorOrFunction =>
isFn(errorOrFunction)
? errorOrFunction
: () => Promise.reject(errorOrFunction)

const fetch = jest.fn(normalizeResponse(''))
fetch.Headers = Headers
fetch.Response = ResponseWrapper
fetch.Response = responseWrapper
fetch.Request = Request
fetch.mockResponse = (bodyOrFunction, init) => fetch.mockImplementation(normalizeResponse(bodyOrFunction, init))
fetch.mockResponse = (bodyOrFunction, init) =>
fetch.mockImplementation(normalizeResponse(bodyOrFunction, init))

fetch.mockReject = errorOrFunction =>
fetch.mockImplementation(normalizeError(errorOrFunction))

fetch.mockReject = errorOrFunction => fetch.mockImplementation(normalizeError(errorOrFunction))
fetch.mockAbort = () => fetch.mockImplementation(abortAsync)
fetch.mockAbortOnce = () => fetch.mockImplementationOnce(abortAsync)

const mockResponseOnce = (bodyOrFunction, init) => fetch.mockImplementationOnce(normalizeResponse(bodyOrFunction, init))
const mockResponseOnce = (bodyOrFunction, init) =>
fetch.mockImplementationOnce(normalizeResponse(bodyOrFunction, init))

fetch.mockResponseOnce = mockResponseOnce

fetch.once = mockResponseOnce

fetch.mockRejectOnce = errorOrFunction => fetch.mockImplementationOnce(normalizeError(errorOrFunction))
fetch.mockRejectOnce = errorOrFunction =>
fetch.mockImplementationOnce(normalizeError(errorOrFunction))

fetch.mockResponses = (...responses) => {
responses.forEach(([bodyOrFunction, init]) => fetch.mockImplementationOnce(normalizeResponse(bodyOrFunction, init)))
responses.forEach(response => {
if (Array.isArray(response)) {
const [body, init] = response
fetch.mockImplementationOnce(normalizeResponse(body, init))
} else {
fetch.mockImplementationOnce(normalizeResponse(response))
}
})
return fetch
}

fetch.isMocking = isMocking

fetch.mockIf = (urlOrPredicate, bodyOrFunction, init) => {
isMocking.mockImplementation(requestMatches(urlOrPredicate))
if (bodyOrFunction) {
fetch.mockResponse(bodyOrFunction, init)
}
return fetch
}

fetch.dontMockIf = (urlOrPredicate, bodyOrFunction, init) => {
isMocking.mockImplementation(requestNotMatches(urlOrPredicate))
if (bodyOrFunction) {
fetch.mockResponse(bodyOrFunction, init)
}
return fetch
}

fetch.mockOnceIf = (urlOrPredicate, bodyOrFunction, init) => {
isMocking.mockImplementationOnce(requestMatches(urlOrPredicate))
if (bodyOrFunction) {
mockResponseOnce(bodyOrFunction, init)
}
return fetch
}

fetch.dontMockOnceIf = (urlOrPredicate, bodyOrFunction, init) => {
isMocking.mockImplementationOnce(requestNotMatches(urlOrPredicate))
if (bodyOrFunction) {
mockResponseOnce(bodyOrFunction, init)
}
return fetch
}

fetch.dontMock = () => {
isMocking.mockImplementation(() => false)
return fetch
}

fetch.dontMockOnce = () => {
isMocking.mockImplementationOnce(() => false)
return fetch
}

fetch.doMock = (bodyOrFunction, init) => {
isMocking.mockImplementation(() => true)
if (bodyOrFunction) {
fetch.mockResponse(bodyOrFunction, init)
}
return fetch
}

fetch.doMockOnce = (bodyOrFunction, init) => {
isMocking.mockImplementationOnce(() => true)
if (bodyOrFunction) {
mockResponseOnce(bodyOrFunction, init)
}
return fetch
}

fetch.resetMocks = () => {
fetch.mockReset()
isMocking.mockReset()

// reset to default implementation with each reset
fetch.mockImplementation(normalizeResponse(''))
fetch.doMock()
fetch.isMocking = isMocking
}

// Default mock is just a empty string.
fetch.mockResponse('')
fetch.enableMocks = () => {
global.fetchMock = global.fetch = fetch
try {
jest.setMock('node-fetch', fetch)
} catch (error) {
//ignore
}
}

fetch.disableMocks = () => {
global.fetch = crossFetch
try {
jest.dontMock('node-fetch')
} catch (error) {
//ignore
}
}

module.exports = fetch
19 changes: 14 additions & 5 deletions tests/api.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'cross-fetch/polyfill'
require('cross-fetch/polyfill')

export async function APIRequest(who) {
async function APIRequest(who) {
if (who === 'facebook') {
const call1 = fetch('https://facebook.com/someOtherResource').then(res =>
res.json()
Expand All @@ -14,16 +14,18 @@ export async function APIRequest(who) {
}
}

export function APIRequest2(who) {
function APIRequest2(who) {
if (who === 'google') {
return fetch('https://google.com').then(res => res.json())
} else {
return 'no argument provided'
}
}

export function request() {
return fetch('https://randomuser.me/api', {})
const defaultRequestUri = 'https://randomuser.me/api'

function request(uri = defaultRequestUri) {
return fetch(uri, {})
.then(response => {
const contentType = response.headers.get('content-type')

Expand All @@ -46,3 +48,10 @@ export function request() {
throw new Error(errorData.error)
})
}

module.exports = {
request,
APIRequest,
APIRequest2,
defaultRequestUri
}

0 comments on commit a0a22de

Please sign in to comment.