fetch-retry
Advanced tools
Comparing version 2.0.0 to 2.1.0
66
index.js
@@ -10,18 +10,23 @@ 'use strict'; | ||
if (options && options.retries) { | ||
retries = options.retries; | ||
if (options && options.retries !== undefined) { | ||
if (isPositiveInteger(options.retries)) { | ||
retries = options.retries; | ||
} else { | ||
throw new ArgumentError('retries must be a positive integer'); | ||
} | ||
} | ||
if (options && options.retryDelay) { | ||
retryDelay = options.retryDelay; | ||
if (options && options.retryDelay !== undefined) { | ||
if (isPositiveInteger(options.retryDelay) || (typeof options.retryDelay === 'function')) { | ||
retryDelay = options.retryDelay; | ||
} else { | ||
throw new ArgumentError('retryDelay must be a positive integer or a function returning a positive integer'); | ||
} | ||
} | ||
if (options && options.retryOn) { | ||
if (options.retryOn instanceof Array) { | ||
if (Array.isArray(options.retryOn) || (typeof options.retryOn === 'function')) { | ||
retryOn = options.retryOn; | ||
} else { | ||
throw { | ||
name: 'ArgumentError', | ||
message: 'retryOn property expects an array' | ||
} | ||
throw new ArgumentError('retryOn property expects an array or function'); | ||
} | ||
@@ -31,10 +36,16 @@ } | ||
return new Promise(function(resolve, reject) { | ||
var wrappedFetch = function(n) { | ||
var wrappedFetch = function(attempt) { | ||
fetch(url, options) | ||
.then(function(response) { | ||
if (retryOn.indexOf(response.status) === -1) { | ||
if (Array.isArray(retryOn) && retryOn.indexOf(response.status) === -1) { | ||
resolve(response); | ||
} else if (typeof retryOn === 'function') { | ||
if (retryOn(attempt, null, response)) { | ||
retry(attempt, null, response); | ||
} else { | ||
resolve(response); | ||
} | ||
} else { | ||
if (n > 0) { | ||
retry(n); | ||
if (attempt < retries) { | ||
retry(attempt, null, response); | ||
} else { | ||
@@ -46,4 +57,10 @@ resolve(response); | ||
.catch(function(error) { | ||
if (n > 0) { | ||
retry(n); | ||
if (typeof retryOn === 'function') { | ||
if (retryOn(attempt, error, null)) { | ||
retry(attempt, error, null); | ||
} else { | ||
reject(error); | ||
} | ||
} else if (attempt < retries) { | ||
retry(attempt, error, null); | ||
} else { | ||
@@ -55,10 +72,21 @@ reject(error); | ||
function retry(n) { | ||
function retry(attempt, error, response) { | ||
var delay = (typeof retryDelay === 'function') ? | ||
retryDelay(attempt, error, response) : retryDelay; | ||
setTimeout(function() { | ||
wrappedFetch(--n); | ||
}, retryDelay); | ||
wrappedFetch(++attempt); | ||
}, delay); | ||
} | ||
wrappedFetch(retries); | ||
wrappedFetch(0); | ||
}); | ||
}; | ||
function isPositiveInteger(value) { | ||
return Number.isInteger(value) && value >= 0; | ||
} | ||
function ArgumentError(message) { | ||
this.name = 'ArgumentError'; | ||
this.message = message; | ||
} |
{ | ||
"name": "fetch-retry", | ||
"version": "2.0.0", | ||
"version": "2.1.0", | ||
"description": "Adds retry functionality to the Fetch API", | ||
@@ -8,3 +8,3 @@ "repository": "https://github.com/jonbern/fetch-retry.git", | ||
"scripts": { | ||
"test": "mocha test/**/**.js", | ||
"test": "nyc mocha test/**/**.js", | ||
"integration-test": "mocha test/integration/" | ||
@@ -22,15 +22,17 @@ }, | ||
"dependencies": { | ||
"es6-promise": "^4.2.4", | ||
"es6-promise": "^4.2.6", | ||
"isomorphic-fetch": "^2.2.1" | ||
}, | ||
"devDependencies": { | ||
"body-parser": "^1.18.3", | ||
"chai": "^4.1.2", | ||
"body-parser": "^1.19.0", | ||
"chai": "^4.2.0", | ||
"chai-as-promised": "^7.1.1", | ||
"eslint": "^5.16.0", | ||
"expectations": "^0.7.1", | ||
"express": "^4.16.2", | ||
"express": "^4.16.4", | ||
"mocha": "^5.2.0", | ||
"proxyquire": "^2.0.1", | ||
"sinon": "^6.1.4" | ||
"nyc": "^14.0.0", | ||
"proxyquire": "^2.1.0", | ||
"sinon": "^6.3.5" | ||
} | ||
} |
@@ -16,5 +16,5 @@ # fetch-retry | ||
## Example | ||
`fetch-retry` is used the same way as `fetch`, but also accepts `retries` and `retryDelay` on the `options` object. | ||
`fetch-retry` is used the same way as `fetch`, but also accepts `retries`, `retryDelay`, and `retryOn` on the `options` object. | ||
These properties are optional, and when omitted will default to 3 retries and a 1000ms retry delay. | ||
These properties are optional, and when omitted will default to 3 retries, a 1000ms retry delay, and to retry only on network errors. | ||
@@ -39,4 +39,20 @@ ```javascript | ||
## Example: Exponential backoff | ||
The default behavior of `fetch-retry` is to wait a fixed amount of time between attempts, but it is also possible to customize this by passing a function as the `retryDelay` option. The function is supplied three arguments: `attempt` (starting at 0), `error` (in case of a network error), and `response`. It must return a number indicating the delay. | ||
```javascript | ||
fetch(url, { | ||
retryDelay: function(attempt, error, response) { | ||
return Math.pow(2, attempt) * 1000; // 1000, 2000, 4000 | ||
} | ||
}).then(function(response) { | ||
return response.json(); | ||
}).then(function(json) { | ||
// do something with the result | ||
console.log(json); | ||
}); | ||
``` | ||
## Example: Retry on 503 (Service Unavailable) | ||
The default behavior of `fetch-retry` is to only retry requests on network related issues, but it is also possible to configure it to retry on specific HTTP status codes. This is done by using the `retryOn` property, which expects an array of HTTP status codes. | ||
The default behavior of `fetch-retry` is to only retry requests on network related issues, but it is also possible to configure it to retry on specific HTTP status codes. This is done by using the `retryOn` property, which expects an array of HTTP status codes. | ||
@@ -55,1 +71,21 @@ ```javascript | ||
``` | ||
## Example: Retry custom behavior | ||
The `retryOn` option may also be specified as a function, in which case it will be supplied three arguments: `attempt` (starting at 0), `error` (in case of a network error), and `response`. Return a truthy value from this function in order to trigger a retry, any falsy value will result in the call to fetch either resolving (in case the last attempt resulted in a response), or rejecting (in case the last attempt resulted in an error). | ||
```javascript | ||
fetch(url, { | ||
retryOn: function(attempt, error, response) { | ||
// retry on any network error, or 4xx or 5xx status codes | ||
if (error !== null || response.status >= 400) { | ||
console.log(`retrying, attempt number ${attempt + 1}`); | ||
return true; | ||
} | ||
}) | ||
.then(function(response) { | ||
return response.json(); | ||
}).then(function(json) { | ||
// do something with the result | ||
console.log(json); | ||
}); | ||
``` |
'use strict'; | ||
const chai = require("chai"); | ||
const chaiAsPromised = require("chai-as-promised"); | ||
const chai = require('chai'); | ||
const chaiAsPromised = require('chai-as-promised'); | ||
chai.use(chaiAsPromised); | ||
@@ -45,3 +45,3 @@ chai.should(); | ||
}); | ||
} | ||
}; | ||
@@ -51,7 +51,7 @@ [200, 503, 404].forEach(statusCode => { | ||
describe('when endpoint returns ' + statusCode, () => { | ||
before(() => { | ||
return setupResponses([statusCode]); | ||
}); | ||
it('does not retry request', () => { | ||
@@ -62,7 +62,7 @@ return fetchRetry(baseUrl) | ||
}); | ||
}); | ||
}); | ||
describe('when configured to retry on a specific HTTP code', () => { | ||
@@ -72,3 +72,3 @@ | ||
const retryOn = [503] | ||
const retryOn = [503]; | ||
@@ -84,3 +84,3 @@ beforeEach(() => { | ||
retryOn | ||
} | ||
}; | ||
@@ -99,3 +99,3 @@ const expectedCallCount = options.retries + 1; | ||
retryOn | ||
} | ||
}; | ||
@@ -105,3 +105,3 @@ const expectedResponse = { | ||
ok: false | ||
} | ||
}; | ||
@@ -122,7 +122,7 @@ return fetchRetry(baseUrl, options) | ||
const retryOnStatus = 503 | ||
const retryOnStatus = 503; | ||
const responses = [503, 503, 200]; | ||
const requestsToRetry = responses | ||
.filter(response => response === retryOnStatus) | ||
.length; | ||
.filter(response => response === retryOnStatus) | ||
.length; | ||
@@ -138,3 +138,3 @@ beforeEach(() => { | ||
retryOn: [retryOnStatus] | ||
} | ||
}; | ||
@@ -153,3 +153,3 @@ const expectedCallCount = requestsToRetry + 1; | ||
retryOn: [retryOnStatus] | ||
} | ||
}; | ||
@@ -159,3 +159,3 @@ const expectedResponse = { | ||
ok: true | ||
} | ||
}; | ||
@@ -180,3 +180,3 @@ return fetchRetry(baseUrl, options) | ||
const retryOn = [503, 404] | ||
const retryOn = [503, 404]; | ||
@@ -192,3 +192,3 @@ beforeEach(() => { | ||
retryOn | ||
} | ||
}; | ||
@@ -195,0 +195,0 @@ const expectedCallCount = options.retries + 1; |
@@ -10,3 +10,3 @@ const express = require('express'); | ||
res.header('Access-Control-Allow-Origin', '*'); | ||
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); | ||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); | ||
next(); | ||
@@ -43,2 +43,2 @@ }); | ||
server = app.listen(3000); | ||
server = app.listen(3000); |
@@ -38,3 +38,3 @@ 'use strict'; | ||
fetch = sinon.stub(); | ||
global.fetch = sinon.stub(); | ||
fetch.onCall(0).returns(deferred1.promise); | ||
@@ -85,3 +85,3 @@ fetch.onCall(1).returns(deferred2.promise); | ||
describe('when #options.retryOn is not an array', () => { | ||
describe('when #options.retryOn is not an array or function', () => { | ||
@@ -94,3 +94,3 @@ it('throws exception', () => { | ||
name: 'ArgumentError', | ||
message: 'retryOn property expects an array' | ||
message: 'retryOn property expects an array or function' | ||
}); | ||
@@ -121,44 +121,19 @@ }); | ||
describe('when #options.retries=3 (default)', function() { | ||
describe('#options.retries', function() { | ||
beforeEach(function() { | ||
thenCallback = sinon.spy(); | ||
catchCallback = sinon.spy(); | ||
describe('when #options.retries=3 (default)', function() { | ||
fetchRetry('http://someurl') | ||
.then(thenCallback) | ||
.catch(catchCallback); | ||
}); | ||
describe('when first call is a success', function() { | ||
beforeEach(function() { | ||
deferred1.resolve({ status: 200 }); | ||
}); | ||
thenCallback = sinon.spy(); | ||
catchCallback = sinon.spy(); | ||
describe('when resolved', function() { | ||
it('invokes the then callback', function() { | ||
expect(thenCallback.called).toBe(true); | ||
}); | ||
it('calls fetch once', function() { | ||
expect(fetch.callCount).toBe(1); | ||
}); | ||
fetchRetry('http://someurl') | ||
.then(thenCallback) | ||
.catch(catchCallback); | ||
}); | ||
}); | ||
describe('when first call is a success', function() { | ||
describe('when first call is a failure', function() { | ||
beforeEach(function() { | ||
deferred1.reject(); | ||
}); | ||
describe('when second call is a succcess', function() { | ||
beforeEach(function() { | ||
clock.tick(delay); | ||
deferred2.resolve({ status: 200 }); | ||
deferred1.resolve({ status: 200 }); | ||
}); | ||
@@ -172,4 +147,4 @@ | ||
it('calls fetch twice', function() { | ||
expect(fetch.callCount).toBe(2); | ||
it('calls fetch once', function() { | ||
expect(fetch.callCount).toBe(1); | ||
}); | ||
@@ -181,14 +156,13 @@ | ||
describe('when second call is a failure', function() { | ||
describe('when first call is a failure', function() { | ||
beforeEach(function() { | ||
deferred2.reject(); | ||
clock.tick(delay); | ||
deferred1.reject(); | ||
}); | ||
describe('when third call is a success', function() { | ||
describe('when second call is a success', function() { | ||
beforeEach(function() { | ||
deferred3.resolve({ status: 200 }); | ||
clock.tick(delay); | ||
deferred2.resolve({ status: 200 }); | ||
}); | ||
@@ -202,4 +176,4 @@ | ||
it('calls fetch three times', function() { | ||
expect(fetch.callCount).toBe(3); | ||
it('calls fetch twice', function() { | ||
expect(fetch.callCount).toBe(2); | ||
}); | ||
@@ -211,13 +185,13 @@ | ||
describe('when third call is a failure', function() { | ||
describe('when second call is a failure', function() { | ||
beforeEach(function() { | ||
deferred3.reject(); | ||
deferred2.reject(); | ||
clock.tick(delay); | ||
}); | ||
describe('when fourth call is a success', function() { | ||
describe('when third call is a success', function() { | ||
beforeEach(function() { | ||
deferred4.resolve({ status: 200 }); | ||
deferred3.resolve({ status: 200 }); | ||
clock.tick(delay); | ||
@@ -232,4 +206,4 @@ }); | ||
it('calls fetch four times', function() { | ||
expect(fetch.callCount).toBe(4); | ||
it('calls fetch three times', function() { | ||
expect(fetch.callCount).toBe(3); | ||
}); | ||
@@ -241,17 +215,26 @@ | ||
describe('when fourth call is a failure', function() { | ||
describe('when third call is a failure', function() { | ||
beforeEach(function() { | ||
deferred4.reject(); | ||
deferred3.reject(); | ||
clock.tick(delay); | ||
}); | ||
describe('when rejected', function() { | ||
describe('when fourth call is a success', function() { | ||
it('invokes the catch callback', function() { | ||
expect(catchCallback.called).toBe(true); | ||
beforeEach(function() { | ||
deferred4.resolve({ status: 200 }); | ||
clock.tick(delay); | ||
}); | ||
it('does not call fetch again', function() { | ||
expect(fetch.callCount).toBe(4); | ||
describe('when resolved', function() { | ||
it('invokes the then callback', function() { | ||
expect(thenCallback.called).toBe(true); | ||
}); | ||
it('calls fetch four times', function() { | ||
expect(fetch.callCount).toBe(4); | ||
}); | ||
}); | ||
@@ -261,2 +244,23 @@ | ||
describe('when fourth call is a failure', function() { | ||
beforeEach(function() { | ||
deferred4.reject(); | ||
clock.tick(delay); | ||
}); | ||
describe('when rejected', function() { | ||
it('invokes the catch callback', function() { | ||
expect(catchCallback.called).toBe(true); | ||
}); | ||
it('does not call fetch again', function() { | ||
expect(fetch.callCount).toBe(4); | ||
}); | ||
}); | ||
}); | ||
}); | ||
@@ -270,31 +274,82 @@ | ||
}); | ||
describe('when #options.retries=1', function() { | ||
describe('when #options.retries=1', function() { | ||
beforeEach(function() { | ||
thenCallback = sinon.spy(); | ||
catchCallback = sinon.spy(); | ||
beforeEach(function() { | ||
thenCallback = sinon.spy(); | ||
catchCallback = sinon.spy(); | ||
fetchRetry('http://someurl', { retries: 1 }) | ||
.then(thenCallback) | ||
.catch(catchCallback); | ||
}); | ||
fetchRetry('http://someurl', { retries: 1 }) | ||
.then(thenCallback) | ||
.catch(catchCallback); | ||
}); | ||
describe('when first call is a success', function() { | ||
describe('when first call is a success', function() { | ||
beforeEach(function() { | ||
deferred1.resolve({ status: 200 }); | ||
}); | ||
beforeEach(function() { | ||
deferred1.resolve({ status: 200 }); | ||
describe('when resolved', function() { | ||
it('invokes the then callback', function() { | ||
expect(thenCallback.called).toBe(true); | ||
}); | ||
it('calls fetch once', function() { | ||
expect(fetch.callCount).toBe(1); | ||
}); | ||
}); | ||
}); | ||
describe('when resolved', function() { | ||
describe('when first call is a failure', function() { | ||
it('invokes the then callback', function() { | ||
expect(thenCallback.called).toBe(true); | ||
beforeEach(function() { | ||
deferred1.reject(); | ||
clock.tick(delay); | ||
}); | ||
it('calls fetch once', function() { | ||
expect(fetch.callCount).toBe(1); | ||
describe('when second call is a success', function() { | ||
beforeEach(function() { | ||
deferred2.resolve({ status: 200 }); | ||
clock.tick(delay); | ||
}); | ||
describe('when resolved', function() { | ||
it('invokes the then callback', function() { | ||
expect(thenCallback.called).toBe(true); | ||
}); | ||
it('calls fetch twice', function() { | ||
expect(fetch.callCount).toBe(2); | ||
}); | ||
}); | ||
}); | ||
describe('when second call is a failure', function() { | ||
beforeEach(function() { | ||
deferred2.reject(); | ||
clock.tick(delay); | ||
}); | ||
describe('when rejected', function() { | ||
it('invokes the catch callback', function() { | ||
expect(catchCallback.called).toBe(true); | ||
}); | ||
it('does not call fetch again', function() { | ||
expect(fetch.callCount).toBe(2); | ||
}); | ||
}); | ||
}); | ||
}); | ||
@@ -304,14 +359,17 @@ | ||
describe('when first call is a failure', function() { | ||
describe('when #options.retries=0', function() { | ||
beforeEach(function() { | ||
deferred1.reject(); | ||
clock.tick(delay); | ||
thenCallback = sinon.spy(); | ||
catchCallback = sinon.spy(); | ||
fetchRetry('http://someurl', { retries: 0 }) | ||
.then(thenCallback) | ||
.catch(catchCallback); | ||
}); | ||
describe('when second call is a succcess', function() { | ||
describe('when first call is a success', function() { | ||
beforeEach(function() { | ||
deferred2.resolve({ status: 200 }); | ||
clock.tick(delay); | ||
deferred1.resolve({ status: 200 }); | ||
}); | ||
@@ -325,4 +383,4 @@ | ||
it('calls fetch twice', function() { | ||
expect(fetch.callCount).toBe(2); | ||
it('calls fetch once', function() { | ||
expect(fetch.callCount).toBe(1); | ||
}); | ||
@@ -334,10 +392,9 @@ | ||
describe('when second call is a failure', function() { | ||
describe('when first call is a failure', function() { | ||
beforeEach(function() { | ||
deferred2.reject(); | ||
clock.tick(delay); | ||
deferred1.reject(); | ||
}); | ||
describe('when rejected', function() { | ||
describe('when rejected', () => { | ||
@@ -348,6 +405,20 @@ it('invokes the catch callback', function() { | ||
it('does not call fetch again', function() { | ||
expect(fetch.callCount).toBe(2); | ||
}); | ||
}); | ||
}); | ||
}); | ||
describe('when #options.retries is not a a positive integer', () => { | ||
['1', -1, 'not a number', null].forEach(invalidRetries => { | ||
it('throws error', () => { | ||
const expectedError = { | ||
name: 'ArgumentError', | ||
message: 'retries must be a positive integer' | ||
}; | ||
expect(() => { | ||
fetchRetry('http://someurl', { retries: invalidRetries }); | ||
}).toThrow(expectedError); | ||
}); | ||
@@ -361,33 +432,88 @@ | ||
describe('when #options.retryDelay is provided', function() { | ||
describe('#options.retryDelay', function() { | ||
var options; | ||
var retryDelay; | ||
describe('when #options.retryDelay is a number', function() { | ||
beforeEach(function() { | ||
retryDelay = 5000; | ||
options = { | ||
retryDelay: retryDelay | ||
}; | ||
var options; | ||
var retryDelay; | ||
thenCallback = sinon.spy(); | ||
beforeEach(function() { | ||
retryDelay = 5000; | ||
options = { | ||
retryDelay: retryDelay | ||
}; | ||
fetchRetry('http://someUrl', options) | ||
.then(thenCallback) | ||
thenCallback = sinon.spy(); | ||
fetchRetry('http://someUrl', options) | ||
.then(thenCallback); | ||
}); | ||
describe('when first call is unsuccessful', function() { | ||
beforeEach(function() { | ||
deferred1.reject(); | ||
}); | ||
describe('after specified time', function() { | ||
beforeEach(function() { | ||
clock.tick(retryDelay); | ||
}); | ||
it('invokes fetch again', function() { | ||
expect(fetch.callCount).toBe(2); | ||
}); | ||
}); | ||
describe('after less than specified time', function() { | ||
beforeEach(function() { | ||
clock.tick(1000); | ||
}); | ||
it('does not invoke fetch again', function() { | ||
expect(fetch.callCount).toBe(1); | ||
}); | ||
}); | ||
}); | ||
}); | ||
describe('when first call is unsuccessful', function() { | ||
describe('when #options.retryDelay is 0', function() { | ||
var options; | ||
var retryDelay; | ||
beforeEach(function() { | ||
deferred1.reject(); | ||
retryDelay = 0; | ||
options = { | ||
retryDelay: retryDelay | ||
}; | ||
thenCallback = sinon.spy(); | ||
fetchRetry('http://someUrl', options) | ||
.then(thenCallback); | ||
}); | ||
describe('after specified time', function() { | ||
describe('when first call is unsuccessful', function() { | ||
beforeEach(function() { | ||
clock.tick(retryDelay); | ||
deferred1.reject(); | ||
}); | ||
it('invokes fetch again', function() { | ||
expect(fetch.callCount).toBe(2); | ||
describe('after one event loop tick', function() { | ||
beforeEach(function() { | ||
clock.tick(0); | ||
}); | ||
it('invokes fetch again', function() { | ||
expect(fetch.callCount).toBe(2); | ||
}); | ||
}); | ||
@@ -397,12 +523,85 @@ | ||
describe('after less than specified time', function() { | ||
}); | ||
describe('when #options.retryDelay is not a a positive integer', () => { | ||
['1', -1, 'not a number', null].forEach(invalidDelay => { | ||
it('throws error', () => { | ||
const expectedError = { | ||
name: 'ArgumentError', | ||
message: 'retryDelay must be a positive integer or a function returning a positive integer' | ||
}; | ||
expect(() => { | ||
fetchRetry('http://someurl', { retryDelay: invalidDelay }); | ||
}).toThrow(expectedError); | ||
}); | ||
}); | ||
}); | ||
describe('when #options.retryDelay is a function', function() { | ||
var options; | ||
var retryDelay; | ||
beforeEach(function() { | ||
retryDelay = sinon.stub().returns(5000); | ||
options = { | ||
retryDelay: retryDelay | ||
}; | ||
thenCallback = sinon.spy(); | ||
fetchRetry('http://someUrl', options) | ||
.then(thenCallback); | ||
}); | ||
describe('when first call is unsuccessful', function() { | ||
beforeEach(function() { | ||
clock.tick(1000); | ||
deferred1.reject(new Error('first error')); | ||
}); | ||
it('does not invoke fetch again', function() { | ||
expect(fetch.callCount).toBe(1); | ||
describe('when the second call is a success', function() { | ||
beforeEach(function() { | ||
deferred2.resolve({ status: 200 }); | ||
clock.tick(5000); | ||
}); | ||
it('invokes the retryDelay function', function() { | ||
expect(retryDelay.called).toBe(true); | ||
expect(retryDelay.lastCall.args[0]).toEqual(0); | ||
expect(retryDelay.lastCall.args[1].message).toEqual('first error'); | ||
}); | ||
}); | ||
describe('when second call is a failure', function() { | ||
beforeEach(function() { | ||
deferred2.reject(new Error('second error')); | ||
clock.tick(5000); | ||
}); | ||
describe('when the third call is a success', function() { | ||
beforeEach(function() { | ||
deferred3.resolve({ status: 200 }); | ||
clock.tick(5000); | ||
}); | ||
it('invokes the retryDelay function again', function() { | ||
expect(retryDelay.callCount).toBe(2); | ||
expect(retryDelay.lastCall.args[0]).toEqual(1); | ||
expect(retryDelay.lastCall.args[1].message).toEqual('second error'); | ||
}); | ||
}); | ||
}); | ||
}); | ||
@@ -414,2 +613,263 @@ | ||
describe('#options.retryOn', () => { | ||
describe('when #options.retryOn is an array', () => { | ||
var options; | ||
var retryOn; | ||
beforeEach(function() { | ||
retryOn = [503, 404]; | ||
options = { | ||
retryOn: retryOn | ||
}; | ||
thenCallback = sinon.spy(); | ||
catchCallback = sinon.spy(); | ||
fetchRetry('http://someUrl', options) | ||
.then(thenCallback) | ||
.catch((catchCallback)); | ||
}); | ||
describe('when first fetch is resolved with status code specified in retryOn array', () => { | ||
beforeEach(() => { | ||
deferred1.resolve({status: 503}); | ||
}); | ||
describe('after specified delay', () => { | ||
beforeEach(() => { | ||
clock.tick(delay); | ||
}); | ||
it('retries fetch', () => { | ||
expect(fetch.callCount).toBe(2); | ||
}); | ||
describe('when second fetch resolves with a different status code', () => { | ||
beforeEach(() => { | ||
deferred2.resolve({status: 200}); | ||
}); | ||
describe('when resolved', () => { | ||
it('invokes the then callback', function() { | ||
expect(thenCallback.called).toBe(true); | ||
}); | ||
it('has called fetch twice', function() { | ||
expect(fetch.callCount).toBe(2); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
describe('when #options.retryOn is a function', function() { | ||
var options; | ||
var retryOn; | ||
beforeEach(function() { | ||
retryOn = sinon.stub(); | ||
options = { | ||
retryOn: retryOn | ||
}; | ||
thenCallback = sinon.spy(); | ||
catchCallback = sinon.spy(); | ||
fetchRetry('http://someUrl', options) | ||
.then(thenCallback) | ||
.catch((catchCallback)); | ||
}); | ||
describe('when first attempt is rejected due to network error', function() { | ||
describe('when #retryOn() returns true', () => { | ||
beforeEach(function() { | ||
retryOn.returns(true); | ||
deferred1.reject(new Error('first error')); | ||
}); | ||
describe('when rejected', function() { | ||
it('invokes #retryOn function with an error', function() { | ||
expect(retryOn.called).toBe(true); | ||
expect(retryOn.lastCall.args.length).toBe(3); | ||
expect(retryOn.lastCall.args[0]).toBe(0); | ||
expect(retryOn.lastCall.args[1] instanceof Error).toBe(true); | ||
expect(retryOn.lastCall.args[2]).toBe(null); | ||
}); | ||
describe('after specified time', function() { | ||
beforeEach(function() { | ||
clock.tick(delay); | ||
}); | ||
it('invokes fetch again', function() { | ||
expect(fetch.callCount).toBe(2); | ||
}); | ||
describe('when the second call is unsuccessful', function() { | ||
beforeEach(function() { | ||
deferred2.reject(new Error('second error')); | ||
clock.tick(delay); | ||
}); | ||
describe('when rejected', function() { | ||
it('invokes the #retryOn function twice', function() { | ||
expect(retryOn.callCount).toBe(2); | ||
expect(retryOn.lastCall.args[0]).toBe(1); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
describe('when #retryOn() returns false', () => { | ||
beforeEach(function() { | ||
retryOn.returns(false); | ||
deferred1.reject(new Error('first error')); | ||
}); | ||
describe('when rejected', function() { | ||
it('invokes #retryOn function with an error', function() { | ||
expect(retryOn.called).toBe(true); | ||
expect(retryOn.lastCall.args.length).toBe(3); | ||
expect(retryOn.lastCall.args[0]).toBe(0); | ||
expect(retryOn.lastCall.args[1] instanceof Error).toBe(true); | ||
expect(retryOn.lastCall.args[2]).toBe(null); | ||
}); | ||
describe('after specified time', function() { | ||
beforeEach(function() { | ||
clock.tick(delay); | ||
}); | ||
it('invokes the catch callback', function() { | ||
expect(catchCallback.called).toBe(true); | ||
}); | ||
it('does not call fetch again', function() { | ||
expect(fetch.callCount).toBe(1); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
describe('when first attempt is resolved', function() { | ||
describe('when #retryOn() returns true', () => { | ||
beforeEach(function() { | ||
retryOn.returns(true); | ||
deferred1.resolve({ status: 200 }); | ||
}); | ||
describe('after specified delay', () => { | ||
beforeEach(function() { | ||
clock.tick(delay); | ||
}); | ||
it('calls fetch again', function() { | ||
expect(fetch.callCount).toBe(2); | ||
}); | ||
describe('when second call is resolved', () => { | ||
beforeEach(function() { | ||
deferred2.resolve({ status: 200 }); | ||
clock.tick(delay); | ||
}); | ||
it('invokes the #retryOn function with the response', function() { | ||
expect(retryOn.called).toBe(true); | ||
expect(retryOn.lastCall.args.length).toBe(3); | ||
expect(retryOn.lastCall.args[0]).toBe(0); | ||
expect(retryOn.lastCall.args[1]).toBe(null); | ||
expect(retryOn.lastCall.args[2]).toEqual({ status: 200 }); | ||
}); | ||
}); | ||
}); | ||
}); | ||
describe('when #retryOn() returns false', () => { | ||
beforeEach(function() { | ||
retryOn.returns(false); | ||
deferred1.resolve({ status: 502 }); | ||
}); | ||
describe('when resolved', () => { | ||
it('invokes the then callback', function() { | ||
expect(thenCallback.called).toBe(true); | ||
}); | ||
it('calls fetch 1 time only', function() { | ||
expect(fetch.callCount).toBe(1); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
describe('when #options.retryOn is not an array or function', function() { | ||
var options; | ||
describe('when #options.retryOn is not an array or function', () => { | ||
it('throws exception', () => { | ||
expect(function() { | ||
options.retryOn = 503; | ||
fetchRetry('http://someUrl', options); | ||
}).toThrow({ | ||
name: 'ArgumentError', | ||
message: 'retryOn property expects an array or function' | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
@@ -416,0 +876,0 @@ |
Sorry, the diff of this file is not supported yet
34248
11
877
89
10
28
Updatedes6-promise@^4.2.6