Repository: unexpectedjs/unexpected
Pull request #428 (2017-11-13 23:50:51)

Fix error on promise .and()
Closes https://github.com/unexpectedjs/unexpected/issues/429


Patch:
diff --git a/lib/Unexpected.js b/lib/Unexpected.js
index c46e5567..1f640967 100644
--- a/lib/Unexpected.js
+++ b/lib/Unexpected.js
@@ -5,7 +5,7 @@ var extend = utils.extend;
 var leven = require('leven');
 var makePromise = require('./makePromise');
 var addAdditionalPromiseMethods = require('./addAdditionalPromiseMethods');
-var isPendingPromise = require('./isPendingPromise');
+var wrapPromiseIfNecessary = require('./wrapPromiseIfNecessary');
 var oathbreaker = require('./oathbreaker');
 var UnexpectedError = require('./UnexpectedError');
 var notifyPendingPromise = require('./notifyPendingPromise');
@@ -1207,18 +1207,19 @@ Unexpected.prototype._expect = function expect(context, args) {
 
     try {
         var result = executeExpect(context, subject, testDescriptionString, Array.prototype.slice.call(args, 2));
-        if (isPendingPromise(result)) {
-            that.expect.notifyPendingPromise(result);
-            result = result.then(undefined, function (e) {
-                if (e && e._isUnexpected && context.level === 0) {
-                    that.setErrorMessage(e);
-                }
-                throw e;
-            });
-        } else {
-            if (!result || typeof result.then !== 'function') {
-                result = makePromise.resolve(result);
+        if (utils.isPromise(result)) {
+            result = wrapPromiseIfNecessary(result);
+            if (result.isPending()) {
+                that.expect.notifyPendingPromise(result);
+                result = result.then(undefined, function (e) {
+                    if (e && e._isUnexpected && context.level === 0) {
+                        that.setErrorMessage(e);
+                    }
+                    throw e;
+                });
             }
+        } else {
+            result = makePromise.resolve(result);
         }
         return addAdditionalPromiseMethods(result, that.expect, subject);
     } catch (e) {
diff --git a/lib/createWrappedExpectProto.js b/lib/createWrappedExpectProto.js
index f105df3e..4f2fba5f 100644
--- a/lib/createWrappedExpectProto.js
+++ b/lib/createWrappedExpectProto.js
@@ -1,9 +1,9 @@
 var createStandardErrorMessage = require('./createStandardErrorMessage');
 var makePromise = require('./makePromise');
-var isPendingPromise = require('./isPendingPromise');
+var addAdditionalPromiseMethods = require('./addAdditionalPromiseMethods');
+var wrapPromiseIfNecessary = require('./wrapPromiseIfNecessary');
 var oathbreaker = require('./oathbreaker');
 var UnexpectedError = require('./UnexpectedError');
-var addAdditionalPromiseMethods = require('./addAdditionalPromiseMethods');
 var utils = require('./utils');
 
 function isAssertionArg(arg) {
@@ -89,16 +89,19 @@ module.exports = function createWrappedExpectProto(unexpected) {
 
             try {
                 var result = oathbreaker(callback());
-                if (isPendingPromise(result)) {
-                    result = result.then(undefined, function (e) {
-                        if (e && e._isUnexpected) {
-                            var wrappedError = new UnexpectedError(that, e);
-                            wrappedError.originalError = e.originalError;
-                            throw wrappedError;
-                        }
-                        throw e;
-                    });
-                } else if (!result || typeof result.then !== 'function') {
+                if (utils.isPromise(result)) {
+                    result = wrapPromiseIfNecessary(result);
+                    if (result.isPending()) {
+                        result = result.then(undefined, function (e) {
+                            if (e && e._isUnexpected) {
+                                var wrappedError = new UnexpectedError(that, e);
+                                wrappedError.originalError = e.originalError;
+                                throw wrappedError;
+                            }
+                            throw e;
+                        });
+                    }
+                } else {
                     result = makePromise.resolve(result);
                 }
                 return addAdditionalPromiseMethods(result, that.execute, that.subject);
diff --git a/lib/isPendingPromise.js b/lib/isPendingPromise.js
deleted file mode 100644
index 963bf7bc..00000000
--- a/lib/isPendingPromise.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = function isPendingPromise(obj) {
-    return obj && typeof obj.then === 'function' && typeof obj.isPending === 'function' && obj.isPending();
-};
diff --git a/lib/utils.js b/lib/utils.js
index 781233d3..120859e1 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -24,6 +24,10 @@ var utils = module.exports = {
         return Object.prototype.toString.call(ar) === '[object Array]';
     },
 
+    isPromise: function (obj) {
+        return obj && typeof obj.then === 'function';
+    },
+
     isRegExp: function (re) {
         return (Object.prototype.toString.call(re) === '[object RegExp]');
     },
diff --git a/lib/wrapPromiseIfNecessary.js b/lib/wrapPromiseIfNecessary.js
new file mode 100644
index 00000000..afece1fc
--- /dev/null
+++ b/lib/wrapPromiseIfNecessary.js
@@ -0,0 +1,12 @@
+var makePromise = require('./makePromise');
+
+module.exports = function (promise) {
+    if (typeof promise.isPending !== 'function') {
+        // wrap any non-Unexpected promise
+        return makePromise(function () {
+            return promise;
+        });
+    } else {
+        return promise;
+    }
+};


Test patch:
diff --git a/test/api/shift.spec.js b/test/api/shift.spec.js
index 34dd81c3..ed438a55 100644
--- a/test/api/shift.spec.js
+++ b/test/api/shift.spec.js
@@ -202,4 +202,33 @@ describe('expect.shift', function () {
             );
         });
     });
+
+    describe('when a non-Unexpected promise is passed to shift', function () {
+        it('should allow a subsequent .and()', function () {
+            var clonedExpect = expect.clone().addAssertion('promisified', function (expect, subject) {
+                return expect.shift(new Promise(subject));
+            });
+            return clonedExpect(resolve => {
+                setTimeout(function () {
+                    resolve('usefully');
+                }, 100);
+            }, 'promisified').and('to be truthy');
+        });
+
+        it('should allow a subsequent .and() within a nested context', function () {
+            var clonedExpect = expect.clone().addAssertion('promisified', function (expect, subject) {
+                return expect.shift(new Promise(subject));
+            }).addAssertion('<function> executed inside an assertion', function (expect, subject) {
+                return subject(expect);
+            });
+
+            return clonedExpect(expect => {
+                expect(resolve => {
+                    setTimeout(function () {
+                        resolve('usefully');
+                    }, 100);
+                }, 'promisified').and('to be truthy');
+            }, 'executed inside an assertion');
+        });
+    });
 });