Last active
May 7, 2018 08:58
-
-
Save jrencz/0bf3917d0a28c2b87dff to your computer and use it in GitHub Desktop.
Quick attempt to add spec-compliant race method to Angular $q
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function () { | |
'use strict'; | |
angular | |
.module('q.race', []) | |
.config(function ($provide) { | |
$provide.decorator('$q', function ($delegate) { | |
$delegate.race = promises => $delegate((resolve, reject) => { | |
const bind = promise => { | |
if (typeof promise === 'object' && promise != null && | |
'then' in promise && 'catch' in promise) { | |
promise | |
.then(result => { | |
resolve(result); | |
return result; | |
}) | |
.catch(reason => { | |
reject(reason); | |
return $delegate.reject(reason); | |
}); | |
} else { | |
throw new TypeError('Expected all promises to be thanables,' + | |
` got '${ typeof promise }'`); | |
} | |
}; | |
if (promises && Symbol.iterator in promises) { | |
for (const promise of promises) { | |
bind(promise); | |
} | |
} else if (typeof promises === 'object' && promises != null) { | |
for (const promiseName in promises) { | |
if (promises.hasOwnProperty(promiseName)) { | |
bind(promises[promiseName]); | |
} | |
} | |
} else { | |
throw new TypeError('Expected promises to be an iterable or a' + | |
` hash, got '${ typeof promises }'`); | |
} | |
}); | |
return $delegate; | |
}); | |
}); | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
it('makes sense to implement `$q.race` as a decorator', () => { | |
'use strict'; | |
inject(function ($q) { | |
// This test will fail if `$q.race` is already implemented (Angular 1.5+) | |
// In such case this module should get removed | |
expect('race' in $q).toBe(false); | |
}); | |
}); | |
describe('Module `q.race`', () => { | |
'use strict'; | |
beforeEach(module('q.race')); | |
describe('$q.race', () => { | |
let $q; | |
let getPromises; | |
beforeEach(inject(function (_$q_) { | |
$q = _$q_; | |
window.installPromiseMatchers(); | |
/** | |
* @param {number} quantity | |
* @param {string} as | |
* @returns {{ | |
* promises: (Array|Object), | |
* resolves: (Array|Object), | |
* rejects: (Array|Object), | |
* }} | |
*/ | |
getPromises = ({quantity, as = 'array'}) => { | |
if (!Number.isInteger(quantity) && quantity > 0) { | |
throw new TypeError('getPromises expects count to be a positive' + | |
' integer'); | |
} | |
const promises = []; | |
const resolves = []; | |
const rejects = []; | |
while (quantity-- > 0) { | |
promises.push($q((resolve, reject) => { | |
resolves.push(resolve); | |
rejects.push(reject); | |
})); | |
} | |
if (as === 'hash') { | |
const reduceToHash = (hash, promise, index) => Object.assign(hash, { | |
[`promise-${ index + 1 }`]: promise, | |
}); | |
return { | |
promises: promises.reduce(reduceToHash, {}), | |
resolves: resolves.reduce(reduceToHash, {}), | |
rejects: rejects.reduce(reduceToHash, {}), | |
}; | |
} | |
return {promises, resolves, rejects}; | |
}; | |
})); | |
it('should be a function', () => { | |
expect($q.race).toBeFunction(); | |
}); | |
it('should return a promise', () => { | |
expect($q.race([]).constructor).toBe($q(() => { | |
}).constructor); | |
}); | |
it('should not resolve nor reject the promise automatically', () => { | |
const {promises} = getPromises({quantity: 3}); | |
const racePromise = $q.race(promises); | |
expect(racePromise).not.toBeResolved(); | |
expect(racePromise).not.toBeRejected(); | |
}); | |
describe('(accepted input)', () => { | |
it('should throw when undefined is given as an input', () => { | |
expect(() => { | |
$q.race(); | |
}).toThrowErrorOfType('TypeError'); | |
}); | |
it('should throw when a primitive is given as an input', () => { | |
expect(() => { | |
$q.race(1); | |
}).toThrowErrorOfType('TypeError'); | |
}); | |
it('should throw when any of the objects given in an iterable or a' + | |
' hash is not a promise', () => { | |
expect(() => { | |
$q.race([1]); | |
}).toThrowErrorOfType('TypeError'); | |
expect(() => { | |
$q.race({promise: 1}); | |
}).toThrowErrorOfType('TypeError'); | |
}); | |
it('should accept an Array of promises as input', () => { | |
const {promises} = getPromises({quantity: 3}); | |
expect(() => { | |
$q.race(promises); | |
}).not.toThrow(); | |
}); | |
it('should accept an Iterable other than Array as input', () => { | |
const {promises} = getPromises({quantity: 3}); | |
expect(() => { | |
$q.race(new Set(promises)); | |
}).not.toThrow(); | |
}); | |
it('should accept an Object as input', () => { | |
const {promises: promisesHash} = getPromises({quantity: 3, as: 'hash'}); | |
expect(promisesHash).not.toBeArray(promisesHash); | |
expect(() => { | |
$q.race(promisesHash); | |
}).not.toThrow(); | |
}); | |
it('should only take own properties into consideration when' + | |
' Object is given as input', () => { | |
const quantity = 3; | |
const { | |
promises: parentHash, | |
resolves: parentResolves, | |
rejects: parentRejects, | |
} = getPromises({quantity, as: 'hash'}); | |
let childResolve; | |
const childHash = Object.assign(Object.create(parentHash), { | |
[`promise-${ quantity + 1 }`]: $q(resolve => { | |
childResolve = resolve; | |
}), | |
}); | |
// Some assumptions about the structure of the `child` | |
for (let i = 1; i <= quantity; i++) { | |
expect(`promise-${ i }` in childHash).toBe(true); | |
expect(`promise-${ i }` in parentHash).toBe(true); | |
} | |
expect(`promise-${ quantity + 1 }` in childHash).toBe(true); | |
expect(`promise-${ quantity + 1 }` in parentHash).toBe(false); | |
const racePromise = $q.race(childHash); | |
// Attempt to resolve with all resolve functions from the parent | |
for (let i = 1; i <= quantity; i++) { | |
parentResolves[`promise-${ i }`]('foo'); | |
} | |
expect(racePromise).not.toBeResolved(); | |
// Attempt to reject with all reject functions from the parent | |
for (let i = 1; i <= quantity; i++) { | |
parentRejects[`promise-${ i }`]('bar'); | |
} | |
expect(racePromise).not.toBeRejected(); | |
const resolution = 'bar'; | |
childResolve(resolution); | |
expect(racePromise).toBeResolvedWith(resolution); | |
}); | |
}); | |
describe('returned promise', () => { | |
it('should get resolved as soon as any of promises given to `rage` gets' + | |
' resolved', () => { | |
const {promises, resolves: [, resolve2nd]} = getPromises({quantity: 3}); | |
const racePromise = $q.race(promises); | |
const resolution = 'foo'; | |
resolve2nd(resolution); | |
expect(racePromise).toBeResolvedWith(resolution); | |
}); | |
it('should get rejected as soon as any of promises given to `rage`' + | |
' gets' + | |
' rejected', () => { | |
const {promises, rejects: [, reject2nd]} = getPromises({quantity: 3}); | |
const racePromise = $q.race(promises); | |
const reason = 'foo'; | |
reject2nd(reason); | |
expect(racePromise).toBeRejectedWith(reason); | |
}); | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment