Skip to content

Instantly share code, notes, and snippets.

@jrencz
Last active May 7, 2018 08:58
Show Gist options
  • Save jrencz/0bf3917d0a28c2b87dff to your computer and use it in GitHub Desktop.
Save jrencz/0bf3917d0a28c2b87dff to your computer and use it in GitHub Desktop.
Quick attempt to add spec-compliant race method to Angular $q
(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;
});
});
})();
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