diff --git a/package.json b/package.json index 9cfdb7c..967b78e 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "pretests-only": "npm run build", "tests-only": "npm run test:quick", "precoverage": "npm run build", - "coverage": "babel-node node_modules/.bin/istanbul cover --report html node_modules/.bin/_mocha -- -R tap test/init.js test/*-test.js", + "coverage": "babel-node node_modules/.bin/istanbul cover --report html node_modules/.bin/_mocha -- -R tap test/init.js test/*-test.js test/**/*-test.js", "postcoverage": "npm run cover:check", "cover:check": "istanbul check-coverage && echo code coverage thresholds met, achievement unlocked!", "test:quick": "babel-node node_modules/.bin/_mocha -R tap test/init.js test/*-test.js" diff --git a/src/environment.js b/src/environment.js index 80ad23d..59e9936 100644 --- a/src/environment.js +++ b/src/environment.js @@ -1,26 +1,5 @@ /* eslint func-names:0 no-extra-parens:0 */ import 'airbnb-js-shims'; -import Promise from 'bluebird'; +import StrictPromise from './utils/strict-promise'; -const es6methods = ['then', 'catch', 'constructor']; -const es6StaticMethods = ['all', 'race', 'resolve', 'reject', 'cast']; - -function isNotMethod(name) { - return !(es6methods.includes(name) || es6StaticMethods.includes(name) || name.charAt(0) === '_'); -} - -function del(obj) { - /* eslint no-param-reassign: 0 */ - return (key) => { delete obj[key]; }; -} - -function toFastProperties(obj) { - (function () {}).prototype = obj; -} - -Object.keys(Promise.prototype).filter(isNotMethod).forEach(del(Promise.prototype)); -Object.keys(Promise).filter(isNotMethod).forEach(del(Promise)); -toFastProperties(Promise); -toFastProperties(Promise.prototype); - -global.Promise = Promise; +global.Promise = StrictPromise; diff --git a/src/utils/strict-promise.js b/src/utils/strict-promise.js new file mode 100644 index 0000000..d0f567c --- /dev/null +++ b/src/utils/strict-promise.js @@ -0,0 +1,69 @@ +/* eslint-disable no-underscore-dangle */ + +import Promise from 'bluebird'; + +export default class StrictPromise { + constructor(executor) { + this._promise = new Promise(executor); + } + + static all(iterable) { + const newPromise = Promise.all(iterable); + + const strictPromise = new StrictPromise((resolve, reject) => { + newPromise.then(resolve, reject); + }); + + return strictPromise; + } + + static race(iterable) { + const newPromise = Promise.race(iterable); + + const strictPromise = new StrictPromise((resolve, reject) => { + newPromise.then(resolve, reject); + }); + + return strictPromise; + } + + static reject(value) { + const newPromise = Promise.reject(value); + + const strictPromise = new StrictPromise((resolve, reject) => { + newPromise.then(resolve, reject); + }); + + return strictPromise; + } + + static resolve(value) { + const newPromise = Promise.resolve(value); + + const strictPromise = new StrictPromise((resolve, reject) => { + newPromise.then(resolve, reject); + }); + + return strictPromise; + } + + then(onFulfilled, onRejected) { + const newPromise = this._promise.then(onFulfilled, onRejected); + + const strictPromise = new StrictPromise((resolve, reject) => { + newPromise.then(resolve, reject); + }); + + return strictPromise; + } + + catch(onRejected) { + const newPromise = this._promise.catch(onRejected); + + const strictPromise = new StrictPromise((resolve, reject) => { + newPromise.then(resolve, reject); + }); + + return strictPromise; + } +} diff --git a/test/environment-test.js b/test/environment-test.js new file mode 100644 index 0000000..21f1a9e --- /dev/null +++ b/test/environment-test.js @@ -0,0 +1,19 @@ +import { assert } from 'chai'; +import '../src/environment'; + +describe('environment', () => { + describe('global promise', () => { + it('has ES6 methods', () => { + assert.isFunction(global.Promise.prototype.then); + assert.isFunction(global.Promise.prototype.catch); + assert.isFunction(global.Promise.prototype.constructor); + }); + + it('has ES6 static methods', () => { + assert.isFunction(global.Promise.all); + assert.isFunction(global.Promise.race); + assert.isFunction(global.Promise.resolve); + assert.isFunction(global.Promise.reject); + }); + }); +}); diff --git a/test/utils/strict-promise-test.js b/test/utils/strict-promise-test.js new file mode 100644 index 0000000..41ebae7 --- /dev/null +++ b/test/utils/strict-promise-test.js @@ -0,0 +1,170 @@ +import { assert } from 'chai'; +import StrictPromise from '../../src/utils/strict-promise'; + +describe('StrictPromise', () => { + describe('static all', () => { + it('resolves when the iterable is empty', (done) => { + StrictPromise.all([]).then(() => { + done(); + }); + }); + + it('resolves when the whole iterable is resolved', (done) => { + const resolveWiths = []; + const messages = [ + 'First resolved.', + 'Second resolved.', + 'Third resolved', + ]; + + StrictPromise.all([ + new StrictPromise((resolve) => { + resolveWiths.push(resolve); + }), + new StrictPromise((resolve) => { + resolveWiths.push(resolve); + }), + new StrictPromise((resolve) => { + resolveWiths.push(resolve); + }), + ]).then((resolvedWiths) => { + assert.strictEqual(resolvedWiths[0], messages[0]); + assert.strictEqual(resolvedWiths[1], messages[1]); + assert.strictEqual(resolvedWiths[2], messages[2]); + done(); + }); + + resolveWiths[0](messages[0]); + resolveWiths[1](messages[1]); + resolveWiths[2](messages[2]); + }); + + it('rejects with the first rejected iterable', (done) => { + let rejectWith; + const message = 'Only one rejected.'; + + StrictPromise.all([ + new StrictPromise(() => {}), + new StrictPromise((resolve, reject) => { + rejectWith = reject; + }), + new StrictPromise(() => {}), + ]).then(() => {}, (rejectedWith) => { + assert.strictEqual(rejectedWith, message); + done(); + }); + + rejectWith(message); + }); + }); + + describe('static race', () => { + it('resolves with the first resolved iterable', (done) => { + let resolveWith; + const message = 'It is a resolved race!'; + + StrictPromise.race([ + new StrictPromise(() => {}), + new StrictPromise((resolve) => { + resolveWith = resolve; + }), + new StrictPromise(() => {}), + ]).then((resolvedWith) => { + assert.strictEqual(resolvedWith, message); + done(); + }); + + resolveWith(message); + }); + + it('rejects with the first rejected iterable', (done) => { + let rejectWith; + const message = 'It is a rejected race!'; + + StrictPromise.race([ + new StrictPromise(() => {}), + new StrictPromise((resolve, reject) => { + rejectWith = reject; + }), + new StrictPromise(() => {}), + ]).then(() => {}, (rejectedWith) => { + assert.strictEqual(rejectedWith, message); + done(); + }); + + rejectWith(message); + }); + }); + + describe('static reject', () => { + it('rejects with a given reason', (done) => { + const message = 'rejected'; + + StrictPromise.reject(message).catch((rejectedWith) => { + assert.strictEqual(rejectedWith, message); + done(); + }); + }); + }); + + describe('static resolve', () => { + it('resolves with a given reason', (done) => { + const message = 'fulfilled'; + + StrictPromise.resolve(message).then((resolvedWith) => { + assert.strictEqual(resolvedWith, message); + done(); + }); + }); + }); + + describe('thennable', () => { + it('has a fulfillment callback', (done) => { + let resolveWith; + const message = 'fulfilled'; + + const promise = new StrictPromise((resolve) => { + resolveWith = resolve; + }); + + promise.then((resolvedWith) => { + assert.strictEqual(resolvedWith, message); + done(); + }); + + resolveWith(message); + }); + + it('has a rejection callback', (done) => { + let rejectWith; + const message = 'rejected'; + + const promise = new StrictPromise((resolve, reject) => { + rejectWith = reject; + }); + + promise.then(() => {}, (rejectedWith) => { + assert.strictEqual(rejectedWith, message); + done(); + }); + + rejectWith(message); + }); + }); + + it('is catchable', (done) => { + let rejectWith; + const message = 'rejected'; + + const promise = new StrictPromise((resolve, reject) => { + rejectWith = reject; + }); + + promise.catch((rejectedWith) => { + assert.strictEqual(rejectedWith, message); + done(); + }); + + rejectWith(message); + }); +});