Skip to content

Commit 9cb0a5c

Browse files
committed
[js] Fix a promise/a+ compliance bug revealed when running against version
2.1.1 of the compliance test suite.
1 parent e255009 commit 9cb0a5c

File tree

4 files changed

+107
-47
lines changed

4 files changed

+107
-47
lines changed

javascript/node/selenium-webdriver/CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
* Node v0.12.x users must run with --harmony. _This is the last release that
44
will support v0.12.x_
5+
* FIXED: (Promise/A+ compliance) When a promise is rejected with a thenable,
6+
the promise adopts the thenable as its rejection reason instead of waiting
7+
for it to settle. The previous (incorrect) behavior was hidden by bugs in
8+
the `promises-aplus-tests` compliance test suite that were fixed in version
9+
`2.1.1`.
510
* FIXED: the `webdriver.promise.ControlFlow` now has a consistent execution
611
order for tasks/callbacks scheduled in different turns of the JS event loop.
712
Refer to the `webdriver.promise` documentation for more details.

javascript/webdriver/promise.js

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,38 +1080,40 @@ promise.Promise = goog.defineClass(null, {
10801080
this.parent_ = null;
10811081
this.state_ = PromiseState.BLOCKED;
10821082

1083-
if (promise.Thenable.isImplementation(newValue)) {
1084-
// 2.3.2
1085-
newValue = /** @type {!promise.Thenable} */(newValue);
1086-
newValue.then(
1087-
this.unblockAndResolve_.bind(this, PromiseState.FULFILLED),
1088-
this.unblockAndResolve_.bind(this, PromiseState.REJECTED));
1089-
return;
1083+
if (newState !== PromiseState.REJECTED) {
1084+
if (promise.Thenable.isImplementation(newValue)) {
1085+
// 2.3.2
1086+
newValue = /** @type {!promise.Thenable} */(newValue);
1087+
newValue.then(
1088+
this.unblockAndResolve_.bind(this, PromiseState.FULFILLED),
1089+
this.unblockAndResolve_.bind(this, PromiseState.REJECTED));
1090+
return;
10901091

1091-
} else if (goog.isObject(newValue)) {
1092-
// 2.3.3
1092+
} else if (goog.isObject(newValue)) {
1093+
// 2.3.3
10931094

1094-
try {
1095-
// 2.3.3.1
1096-
var then = newValue['then'];
1097-
} catch (e) {
1098-
// 2.3.3.2
1099-
this.state_ = PromiseState.REJECTED;
1100-
this.value_ = e;
1101-
this.scheduleNotifications_();
1102-
return;
1103-
}
1095+
try {
1096+
// 2.3.3.1
1097+
var then = newValue['then'];
1098+
} catch (e) {
1099+
// 2.3.3.2
1100+
this.state_ = PromiseState.REJECTED;
1101+
this.value_ = e;
1102+
this.scheduleNotifications_();
1103+
return;
1104+
}
11041105

1105-
// NB: goog.isFunction is loose and will accept instanceof Function.
1106-
if (typeof then === 'function') {
1107-
// 2.3.3.3
1108-
this.invokeThen_(newValue, then);
1109-
return;
1106+
// NB: goog.isFunction is loose and will accept instanceof Function.
1107+
if (typeof then === 'function') {
1108+
// 2.3.3.3
1109+
this.invokeThen_(newValue, then);
1110+
return;
1111+
}
11101112
}
11111113
}
11121114

11131115
if (newState === PromiseState.REJECTED &&
1114-
isError(newValue) && newValue.stack && this.stack_) {
1116+
isError(newValue) && newValue.stack && this.stack_) {
11151117
newValue.stack += '\nFrom: ' + (this.stack_.stack || this.stack_);
11161118
}
11171119

javascript/webdriver/test/promise_error_test.js

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -465,26 +465,28 @@ function testThrownPromiseIsHandledSameAsReturningPromise_promiseIsFulfilled() {
465465
}
466466

467467

468-
function testThrownPromiseIsHandledSameAsReturningPromise_promiseIsRejected() {
469-
return webdriver.promise.fulfilled().then(function() {
470-
throw webdriver.promise.rejected(new StubError);
471-
}).then(fail, assertIsStubError);
472-
}
473-
474-
475-
function testTaskThrowsPromise_taskSucceedsIfPromiseIsFulfilled() {
468+
function testTaskThrowsPromise_promiseWasFulfiled() {
469+
var toThrow = webdriver.promise.fulfilled(1234);
476470
flow.execute(function() {
477-
throw webdriver.promise.fulfilled(1234);
471+
throw toThrow;
472+
}).then(fail, function(value) {
473+
assertEquals(toThrow, value);
474+
return toThrow;
478475
}).then(function(value) {
479476
assertEquals(1234, value);
480477
});
481478
return waitForIdle();
482479
}
483480

484481

485-
function testTaskThrowsPromise_taskFailsIfPromiseIsRejected() {
482+
function testTaskThrowsPromise_promiseWasRejected() {
483+
var toThrow = webdriver.promise.rejected(new StubError);
484+
toThrow.thenCatch(goog.nullFunction); // For tearDown.
486485
flow.execute(function() {
487-
throw webdriver.promise.rejected(new StubError);
486+
throw toThrow;
487+
}).then(fail, function(e) {
488+
assertEquals(toThrow, e);
489+
return e;
488490
}).then(fail, assertIsStubError);
489491
return waitForIdle();
490492
}
@@ -842,3 +844,49 @@ function testErrorsInAsyncFunctionsAreReportedAsUnhandledRejection() {
842844
});
843845
});
844846
}
847+
848+
849+
function testDoesNotWaitForValuesThrownFromCallbacksToBeResolved_1() {
850+
var p1 = webdriver.promise.fulfilled();
851+
var reason = webdriver.promise.fulfilled('should not see me');
852+
return p1.then(function() {
853+
throw reason;
854+
}).then(fail, function(e) {
855+
assertEquals(reason, e);
856+
});
857+
}
858+
859+
860+
function testDoesNotWaitForValuesThrownFromCallbacksToBeResolved_2() {
861+
var p1 = webdriver.promise.fulfilled();
862+
var reason = webdriver.promise.rejected('should not see me');
863+
reason.thenCatch(goog.nullFunction); // For tearDown.
864+
return p1.then(function() {
865+
throw reason;
866+
}).then(fail, function(e) {
867+
assertEquals(reason, e);
868+
});
869+
}
870+
871+
872+
function testDoesNotWaitForValuesThrownFromCallbacksToBeResolved_3() {
873+
var p1 = webdriver.promise.fulfilled();
874+
var reason = webdriver.promise.defer();
875+
setTimeout(() => reason.fulfill('should not see me'), 100);
876+
return p1.then(function() {
877+
throw reason.promise;
878+
}).then(fail, function(e) {
879+
assertEquals(reason.promise, e);
880+
});
881+
}
882+
883+
884+
function testDoesNotWaitForValuesThrownFromCallbacksToBeResolved_4() {
885+
var p1 = webdriver.promise.fulfilled();
886+
var reason = {then: function() {}}; // A thenable like object.
887+
return p1.then(function() {
888+
throw reason;
889+
}).then(fail, function(e) {
890+
assertEquals(reason, e);
891+
});
892+
}

javascript/webdriver/test/promise_test.js

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ function testRejectForcesValueToAnError_errorLike() {
574574
}
575575

576576

577-
function testRejectingAPromiseWithAnotherPromiseCreatesAChain_ourPromise() {
577+
function testRejectingAPromiseWithAnotherPromise_ourPromise() {
578578
var d1 = new webdriver.promise.Deferred();
579579
var d2 = new webdriver.promise.Deferred();
580580
var pair1 = callbackPair(assertIsStubError, null);
@@ -583,7 +583,11 @@ function testRejectingAPromiseWithAnotherPromiseCreatesAChain_ourPromise() {
583583
return new StubError;
584584
});
585585

586-
d1.then(pair1.callback, pair1.errback);
586+
d1.then(fail, function(e) {
587+
assertEquals(d2promise, e);
588+
return d2promise;
589+
}).then(pair1.callback, pair1.errback);
590+
587591
var d2promise = d2.then(pair2.callback, pair2.errback);
588592

589593
pair1.assertNeither();
@@ -600,11 +604,7 @@ function testRejectingAPromiseWithAnotherPromiseCreatesAChain_ourPromise() {
600604
}
601605

602606

603-
function testRejectingAPromiseWithAnotherPromiseCreatesAChain_otherPromise() {
604-
var d = new webdriver.promise.Deferred(), callback, errback;
605-
d.then(callback = callbackHelper(assertIsStubError),
606-
errback = callbackHelper());
607-
607+
function testRejectingAPromiseWithAnotherPromise_otherPromise() {
608608
var otherPromise = {
609609
then: function(callback) {
610610
this.callback = callback;
@@ -614,15 +614,20 @@ function testRejectingAPromiseWithAnotherPromiseCreatesAChain_otherPromise() {
614614
}
615615
};
616616

617+
var pair = callbackPair(null, assertIsStubError);
618+
var d = new webdriver.promise.Deferred();
619+
d.promise.then(fail, function(e) {
620+
assertEquals(otherPromise, e);
621+
return otherPromise;
622+
}).then(pair.callback, pair.errback);
623+
617624
d.reject(otherPromise);
618625
clock.tick();
619-
callback.assertNotCalled();
620-
errback.assertNotCalled();
626+
pair.assertNeither();
621627

622628
otherPromise.fulfill(new StubError);
623629
clock.tick();
624-
callback.assertCalled();
625-
errback.assertNotCalled();
630+
pair.assertCallback();
626631
}
627632

628633

0 commit comments

Comments
 (0)