Skip to content

Commit

Permalink
For compliance with the promise spec, promises may no longer resolve …
Browse files Browse the repository at this point in the history
…to themselves.

This required splitting webdriver.WebElement into two types: a concrete WebElement
and a thenable that resolves to the WebElement and acts as a forward proxy for the
WebElement API. This change should be largely transparent to users.
  • Loading branch information
jleyba committed Jul 16, 2014
1 parent c1b1b2a commit d338669
Show file tree
Hide file tree
Showing 6 changed files with 395 additions and 240 deletions.
4 changes: 4 additions & 0 deletions javascript/node/selenium-webdriver/CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## v2.43.0-dev

* Promise A+ compliance: a promise may no longer resolve to itself.
* For consistency with other language bindings, deprecated
`UnhandledAlertError#getAlert` and added `#getAlertText`.
`getAlert` will be removed in `2.45.0`.
* FIXED: 7563: Mocha integration no longer disables timeouts. Default Mocha
timeouts apply (2000 ms) and may be changed using `this.timeout(ms)`.

Expand Down
8 changes: 4 additions & 4 deletions javascript/node/selenium-webdriver/test/_base_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,17 @@ function runClosureTest(file) {
describe(name, function() {
var context = new base.Context(true);
context.closure.document.title = name;
// Null out console so everything loads silently.
context.closure.console = null;
if (process.env.VERBOSE != '1') {
// Null out console so everything loads silently.
context.closure.console = null;
}
context.closure.CLOSURE_IMPORT_SCRIPT(file);

var tc = context.closure.G_testRunner.testCase;
if (!tc) {
tc = new context.closure.goog.testing.TestCase(name);
tc.autoDiscoverTests();
}
// Reset console for running tests.
context.closure.console = console;

var allTests = tc.getTests();
allTests.forEach(function(test) {
Expand Down
149 changes: 119 additions & 30 deletions javascript/webdriver/promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ goog.provide('webdriver.promise.ControlFlow');
goog.provide('webdriver.promise.ControlFlow.Timer');
goog.provide('webdriver.promise.Deferred');
goog.provide('webdriver.promise.Promise');
goog.provide('webdriver.promise.Thenable');

goog.require('goog.array');
goog.require('goog.debug.Error');
Expand All @@ -61,43 +62,32 @@ goog.require('webdriver.stacktrace.Snapshot');


/**
* Represents the eventual value of a completed operation. Each promise may be
* in one of three states: pending, resolved, or rejected. Each promise starts
* in the pending state and may make a single transition to either a
* fulfilled or failed state.
* Thenable is a promise-like object with a {@code then} method which may be
* used to schedule callbacks on a promised value.
*
* <p/>This class is based on the Promise/A proposal from CommonJS. Additional
* functions are provided for API compatibility with Dojo Deferred objects.
*
* @constructor
* @interface
* @template T
* @see http://wiki.commonjs.org/wiki/Promises/A
*/
webdriver.promise.Promise = function() {
};
webdriver.promise.Thenable = function() {};


/**
* Cancels the computation of this promise's value, rejecting the promise in the
* process.
* process. This method is a no-op if the promise has alreayd been resolved.
*
* @param {*} reason The reason this promise is being cancelled. If not an
* {@code Error}, one will be created using the value's string
* representation.
*/
webdriver.promise.Promise.prototype.cancel = function(reason) {
throw new TypeError('Unimplemented function: "cancel"');
};
webdriver.promise.Thenable.prototype.cancel = function(opt_reason) {};


/** @return {boolean} Whether this promise's value is still being computed. */
webdriver.promise.Promise.prototype.isPending = function() {
throw new TypeError('Unimplemented function: "isPending"');
};
webdriver.promise.Thenable.prototype.isPending = function() {};


/**
* Registers listeners for when this instance is resolved. This function most
* overridden by subtypes.
* Registers listeners for when this instance is resolved.
*
* @param {?(function(T): (R|webdriver.promise.Promise.<R>))=} opt_callback The
* function to call if this promise is successfully resolved. The function
Expand All @@ -109,10 +99,8 @@ webdriver.promise.Promise.prototype.isPending = function() {
* resolved with the result of the invoked callback.
* @template R
*/
webdriver.promise.Promise.prototype.then = function(
opt_callback, opt_errback) {
throw new TypeError('Unimplemented function: "then"');
};
webdriver.promise.Thenable.prototype.then = function(
opt_callback, opt_errback) {};


/**
Expand All @@ -139,9 +127,7 @@ webdriver.promise.Promise.prototype.then = function(
* resolved with the result of the invoked callback.
* @template R
*/
webdriver.promise.Promise.prototype.thenCatch = function(errback) {
return this.then(null, errback);
};
webdriver.promise.Thenable.prototype.thenCatch = function(errback) {};


/**
Expand Down Expand Up @@ -183,6 +169,102 @@ webdriver.promise.Promise.prototype.thenCatch = function(errback) {
* with the callback result.
* @template R
*/
webdriver.promise.Thenable.prototype.thenFinally = function(callback) {};


/**
* Property used to flag constructor's as implementing the Thenable interface
* for runtime type checking.
* @private {string}
* @const
*/
webdriver.promise.Thenable.IMPLEMENTED_BY_PROP_ = '$webdriver_Thenable';


/**
* Adds a property to a class prototype to allow runtime checks of whether
* instances of that class implement the Thenable interface. This function will
* also ensure the prototype's {@code then} function is exported from compiled
* code.
* @param {function(new: webdriver.promise.Thenable, ...[?])} ctor The
* constructor whose prototype to modify.
*/
webdriver.promise.Thenable.addImplementation = function(ctor) {
// Based on goog.promise.Thenable.isImplementation.
ctor.prototype['then'] = ctor.prototype.then;
Object.defineProperty(
ctor.prototype,
webdriver.promise.Thenable.IMPLEMENTED_BY_PROP_,
{'value': true, 'enumerable': false});
};


/**
* Checks if an object has been tagged for implementing the Thenable interface
* as defined by {@link webdriver.promise.Thenable.addImplementation}.
* @param {*} object The object to test.
* @return {boolean} Whether the object is an implementation of the Thenable
* interface.
*/
webdriver.promise.Thenable.isImplementation = function(object) {
// Based on goog.promise.Thenable.isImplementation.
if (!object) {
return false;
}
try {
if (COMPILED) {
return !!object[webdriver.promise.Thenable.IMPLEMENTED_BY_PROP_];
}
return !!object.$webdriver_Thenable;
} catch (e) {
return false; // Property access seems to be forbidden.
}
};



/**
* Represents the eventual value of a completed operation. Each promise may be
* in one of three states: pending, resolved, or rejected. Each promise starts
* in the pending state and may make a single transition to either a
* fulfilled or rejected state, at which point the promise is considered
* resolved.
*
* @constructor
* @implements {webdriver.promise.Thenable.<T>}
* @template T
* @see http://promises-aplus.github.io/promises-spec/
*/
webdriver.promise.Promise = function() {};
webdriver.promise.Thenable.addImplementation(webdriver.promise.Promise);


/** @override */
webdriver.promise.Promise.prototype.cancel = function(reason) {
throw new TypeError('Unimplemented function: "cancel"');
};


/** @override */
webdriver.promise.Promise.prototype.isPending = function() {
throw new TypeError('Unimplemented function: "isPending"');
};


/** @override */
webdriver.promise.Promise.prototype.then = function(
opt_callback, opt_errback) {
throw new TypeError('Unimplemented function: "then"');
};


/** @override */
webdriver.promise.Promise.prototype.thenCatch = function(errback) {
return this.then(null, errback);
};


/** @override */
webdriver.promise.Promise.prototype.thenFinally = function(callback) {
return this.then(callback, function(err) {
var value = callback();
Expand Down Expand Up @@ -293,9 +375,15 @@ webdriver.promise.Deferred = function(opt_canceller, opt_flow) {
return;
}

if (newValue === self) {
// See promise a+, 2.3.1
// http://promises-aplus.github.io/promises-spec/#point-48
throw TypeError('A promise may not resolve to itself');
}

state = webdriver.promise.Deferred.State_.BLOCKED;

if (webdriver.promise.isPromise(newValue) && newValue !== self) {
if (webdriver.promise.isPromise(newValue)) {
var onFulfill = goog.partial(notifyAll, newState);
var onReject = goog.partial(
notifyAll, webdriver.promise.Deferred.State_.REJECTED);
Expand Down Expand Up @@ -635,7 +723,7 @@ webdriver.promise.checkedNodeCall = function(fn) {
* @return {!webdriver.promise.Promise} A new promise.
*/
webdriver.promise.when = function(value, opt_callback, opt_errback) {
if (value instanceof webdriver.promise.Promise) {
if (webdriver.promise.Thenable.isImplementation(value)) {
return value.then(opt_callback, opt_errback);
}

Expand Down Expand Up @@ -1558,7 +1646,8 @@ webdriver.promise.ControlFlow.prototype.runInNewFrame_ = function(
newFrame.then(function() {
webdriver.promise.asap(result, callback, errback);
}, function(e) {
if (result instanceof webdriver.promise.Promise && result.isPending()) {
if (webdriver.promise.Thenable.isImplementation(result) &&
result.isPending()) {
result.cancel(e);
e = result;
}
Expand Down
56 changes: 10 additions & 46 deletions javascript/webdriver/test/promise_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -548,39 +548,9 @@ function testResolvingADeferredWithAnotherCopiesTheResolvedValue() {
}


function testCanResolveADeferredWithItself() {
var callback;
function testCannotResolveADeferredWithItself() {
var deferred = new webdriver.promise.Deferred();
deferred.then(callback = callbackHelper(function(d) {
assertEquals(deferred, d);
}));

callback.assertNotCalled();
deferred.fulfill(deferred);
callback.assertCalled();
}


function testResolvingADeferredWithAnotherThatResolvedUponItself() {
var d1 = new webdriver.promise.Deferred();
var d2 = new webdriver.promise.Deferred();
var callback1, callback2;

d1.then(callback1 = callbackHelper(function(value) {
assertEquals(d2, value);
}));

d2.then(callback2 = callbackHelper(function(value) {
assertEquals(d2, value);
}));

d1.fulfill(d2);
callback1.assertNotCalled();
callback2.assertNotCalled();

d2.fulfill(d2);
callback1.assertCalled();
callback2.assertCalled();
assertThrows(goog.bind(deferred.fulfill, deferred, deferred));
}


Expand Down Expand Up @@ -958,20 +928,6 @@ function testFullyResolved_arrayWithPromisedHash() {
}


function testFullyResolved_deferredThatResolvesOnItself() {
var deferred = new webdriver.promise.Deferred();
deferred.fulfill(deferred);

var callbacks = callbackPair(function(resolved) {
assertEquals(deferred, resolved);
});

webdriver.promise.fullyResolved(deferred).
then(callbacks.callback, callbacks.errback);
callbacks.assertCallback();
}


function testFullyResolved_aDomElement() {
var e = document.createElement('div');
var callbacks = callbackPair(function(resolved) {
Expand Down Expand Up @@ -1715,3 +1671,11 @@ function testFilteringAnArray_preservesOrderWhenFilterReturnsPromise() {
});
pair.assertCallback();
}


function testAddThenableImplementation() {
function tmp() {}
assertFalse(webdriver.promise.Thenable.isImplementation(new tmp()));
webdriver.promise.Thenable.addImplementation(tmp);
assertTrue(webdriver.promise.Thenable.isImplementation(new tmp()));
}
Loading

0 comments on commit d338669

Please sign in to comment.