From d33866914dfe0c512b7b9ae8c6916b107b4f1680 Mon Sep 17 00:00:00 2001 From: Jason Leyba Date: Tue, 8 Jul 2014 19:43:52 -0400 Subject: [PATCH] For compliance with the promise spec, promises may no longer resolve 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. --- javascript/node/selenium-webdriver/CHANGES.md | 4 + .../selenium-webdriver/test/_base_test.js | 8 +- javascript/webdriver/promise.js | 149 ++++++++--- javascript/webdriver/test/promise_test.js | 56 +--- javascript/webdriver/test/webdriver_test.js | 170 ++++++------ javascript/webdriver/webdriver.js | 248 ++++++++++++------ 6 files changed, 395 insertions(+), 240 deletions(-) diff --git a/javascript/node/selenium-webdriver/CHANGES.md b/javascript/node/selenium-webdriver/CHANGES.md index 0d5856666a0e9..91f41a9a58dba 100644 --- a/javascript/node/selenium-webdriver/CHANGES.md +++ b/javascript/node/selenium-webdriver/CHANGES.md @@ -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)`. diff --git a/javascript/node/selenium-webdriver/test/_base_test.js b/javascript/node/selenium-webdriver/test/_base_test.js index 7e10e8c304261..a5ba8e798f8f7 100644 --- a/javascript/node/selenium-webdriver/test/_base_test.js +++ b/javascript/node/selenium-webdriver/test/_base_test.js @@ -40,8 +40,10 @@ 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; @@ -49,8 +51,6 @@ function runClosureTest(file) { 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) { diff --git a/javascript/webdriver/promise.js b/javascript/webdriver/promise.js index 88dc37bad1e5c..5b5b0ab5139af 100644 --- a/javascript/webdriver/promise.js +++ b/javascript/webdriver/promise.js @@ -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'); @@ -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. * - *

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.))=} opt_callback The * function to call if this promise is successfully resolved. The function @@ -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) {}; /** @@ -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) {}; /** @@ -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.} + * @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(); @@ -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); @@ -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); } @@ -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; } diff --git a/javascript/webdriver/test/promise_test.js b/javascript/webdriver/test/promise_test.js index 6348704bd242b..1703d3950c2e3 100644 --- a/javascript/webdriver/test/promise_test.js +++ b/javascript/webdriver/test/promise_test.js @@ -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)); } @@ -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) { @@ -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())); +} diff --git a/javascript/webdriver/test/webdriver_test.js b/javascript/webdriver/test/webdriver_test.js index b7ef42d28f8e6..cf0f46fc6229a 100644 --- a/javascript/webdriver/test/webdriver_test.js +++ b/javascript/webdriver/test/webdriver_test.js @@ -387,7 +387,7 @@ function testToWireValue_webElement() { var expected = {}; expected[webdriver.WebElement.ELEMENT_KEY] = 'fefifofum'; - var element = new webdriver.WebElement(STUB_DRIVER, 'fefifofum'); + var element = new webdriver.WebElement(STUB_DRIVER, expected); var callback; webdriver.WebDriver.toWireValue_(element). then(callback = callbackHelper(function(actual) { @@ -398,6 +398,23 @@ function testToWireValue_webElement() { } +function testToWireValue_webElementPromise() { + var expected = {}; + expected[webdriver.WebElement.ELEMENT_KEY] = 'fefifofum'; + + var element = new webdriver.WebElement(STUB_DRIVER, expected); + var elementPromise = new webdriver.WebElementPromise(STUB_DRIVER, + webdriver.promise.fulfilled(element)); + var callback; + webdriver.WebDriver.toWireValue_(elementPromise). + then(callback = callbackHelper(function(actual) { + webdriver.test.testutil.assertObjectEquals(expected, actual); + })); + callback.assertCalled(); + verifyAll(); // Expected by tear down. +} + + function testToWireValue_domElement() { assertThrows( goog.partial(webdriver.WebDriver.toWireValue_, document.body)); @@ -421,7 +438,7 @@ function testToWireValue_arrayWithWebElement() { var elementJson = {}; elementJson[webdriver.WebElement.ELEMENT_KEY] = 'fefifofum'; - var element = new webdriver.WebElement(STUB_DRIVER, 'fefifofum'); + var element = new webdriver.WebElement(STUB_DRIVER, elementJson); var callback; webdriver.WebDriver.toWireValue_([element]). then(callback = callbackHelper(function(actual) { @@ -439,7 +456,7 @@ function testToWireValue_complexArray() { elementJson[webdriver.WebElement.ELEMENT_KEY] = 'fefifofum'; var expected = ['abc', 123, true, elementJson, [123, {'foo': 'bar'}]]; - var element = new webdriver.WebElement(STUB_DRIVER, 'fefifofum'); + var element = new webdriver.WebElement(STUB_DRIVER, elementJson); var input = ['abc', 123, true, element, [123, {'foo': 'bar'}]]; var callback; webdriver.WebDriver.toWireValue_(input). @@ -451,6 +468,24 @@ function testToWireValue_complexArray() { } +function testToWireValue_arrayWithNestedPromises() { + var callback; + webdriver.WebDriver.toWireValue_([ + 'abc', + webdriver.promise.fulfilled([ + 123, + webdriver.promise.fulfilled(true) + ]) + ]).then(callback = callbackHelper(function(actual) { + assertEquals(2, actual.length); + assertEquals('abc', actual[0]); + assertArrayEquals([123, true], actual[1]); + })); + callback.assertCalled(); + verifyAll(); // Expected by tear down. +} + + function testToWireValue_complexHash() { var elementJson = {}; elementJson[webdriver.WebElement.ELEMENT_KEY] = 'fefifofum'; @@ -460,7 +495,7 @@ function testToWireValue_complexHash() { 'sessionId': 'foo' }; - var element = new webdriver.WebElement(STUB_DRIVER, 'fefifofum'); + var element = new webdriver.WebElement(STUB_DRIVER, elementJson); var parameters = { 'script': 'return 1', 'args':['abc', 123, true, element, [123, {'foo': 'bar'}]], @@ -1209,64 +1244,37 @@ function testExecutingACustomFunctionThatReturnsADeferredAction() { testHelper.execute(); } -function runWebElementResolutionTest(resolvedId) { - var id = new webdriver.promise.Deferred(); - - var callback, idCallback; - var element = new webdriver.WebElement(STUB_DRIVER, id.promise); - - webdriver.promise.when(element, - callback = callbackHelper(function(resolvedElement) { - assertEquals(element, resolvedElement); - assertFalse(element.toWireValue().isPending()); - })); - - webdriver.promise.when(element.toWireValue(), - idCallback = callbackHelper(function(id) { - assertEquals('object', goog.typeOf(id)); - assertEquals('foo', id[webdriver.WebElement.ELEMENT_KEY]); +function testWebElementPromise_resolvesWhenUnderlyingElementDoes() { + var el = new webdriver.WebElement(STUB_DRIVER, {'ELEMENT': 'foo'}); + var d = webdriver.promise.defer(); + var callback; + new webdriver.WebElementPromise(STUB_DRIVER, d.promise).then( + callback = callbackHelper(function(e) { + assertEquals(e, el); })); - callback.assertNotCalled(); - idCallback.assertNotCalled(); - - id.fulfill(resolvedId); - + d.fulfill(el); callback.assertCalled(); - idCallback.assertCalled(); verifyAll(); // Make tearDown happy. } -function testWebElement_resolvesWhenTheUnderlyingIdResolves() { - runWebElementResolutionTest('foo'); -} - -function testWebElement_resolvesWhenIdResolvesToElementJsonObject() { - runWebElementResolutionTest({'ELEMENT': 'foo'}); -} - -function testWebElement_resolvesWhenIdResolvesToAnotherElement() { - runWebElementResolutionTest( - new webdriver.WebElement(STUB_DRIVER, {'ELEMENT': 'foo'})); -} - -function testWebElement_resolvesWhenIdResolvesToElementJsonObjectResolvesBeforeCallbacksOnWireValueTrigger() { - var id = new webdriver.promise.Deferred(); +function testWebElement_resolvesBeforeCallbacksOnWireValueTrigger() { + var el = new webdriver.promise.Deferred(); var callback, idCallback; - var element = new webdriver.WebElement(STUB_DRIVER, id.promise); + var element = new webdriver.WebElementPromise(STUB_DRIVER, el.promise); var messages = []; webdriver.promise.when(element, function() { messages.push('element resolved'); }); - webdriver.promise.when(element.toWireValue(), function() { + webdriver.promise.when(element.getId_(), function() { messages.push('wire value resolved'); }); assertArrayEquals([], messages); - id.fulfill('foo'); + el.fulfill(new webdriver.WebElement(STUB_DRIVER, {'ELEMENT': 'foo'})); assertArrayEquals([ 'element resolved', 'wire value resolved' @@ -1278,7 +1286,7 @@ function testWebElement_isRejectedIfUnderlyingIdIsRejected() { var id = new webdriver.promise.Deferred(); var callback, errback; - var element = new webdriver.WebElement(STUB_DRIVER, id.promise); + var element = new webdriver.WebElementPromise(STUB_DRIVER, id.promise); webdriver.promise.when(element, callback = callbackHelper(), @@ -1466,7 +1474,7 @@ function testExecuteScript_webElementArgumentConversion() { var driver = testHelper.createDriver(); driver.executeScript('return 1;', - new webdriver.WebElement(driver, 'fefifofum')); + new webdriver.WebElement(driver, elementJson)); testHelper.execute(); } @@ -1485,7 +1493,7 @@ function testExecuteScript_argumentConversion() { replayAll(); var driver = testHelper.createDriver(); - var element = new webdriver.WebElement(driver, 'fefifofum'); + var element = new webdriver.WebElement(driver, elementJson); driver.executeScript('return 1;', 'abc', 123, true, element, [123, {'foo': 'bar'}]); testHelper.execute(); @@ -1755,7 +1763,7 @@ function testFindElements() { function assertTypeAndId(index) { assertTrue('Not a WebElement at index ' + index, elements[index] instanceof webdriver.WebElement); - elements[index].toWireValue(). + elements[index].getId_(). then(callbacks[index] = callbackHelper(function(id) { webdriver.test.testutil.assertObjectEquals(json[index], id); })); @@ -1797,7 +1805,7 @@ function testFindElements_byJs() { function assertTypeAndId(index) { assertTrue('Not a WebElement at index ' + index, elements[index] instanceof webdriver.WebElement); - elements[index].toWireValue(). + elements[index].getId_(). then(callbacks[index] = callbackHelper(function(id) { webdriver.test.testutil.assertObjectEquals(json[index], id); })); @@ -1843,7 +1851,7 @@ function testFindElements_byJs_filtersOutNonWebElementResponses() { function assertTypeAndId(index, jsonIndex) { assertTrue('Not a WebElement at index ' + index, elements[index] instanceof webdriver.WebElement); - elements[index].toWireValue(). + elements[index].getId_(). then(callbacks[index] = callbackHelper(function(id) { webdriver.test.testutil.assertObjectEquals(json[jsonIndex], id); })); @@ -1878,7 +1886,7 @@ function testFindElements_byJs_convertsSingleWebElementResponseToArray() { then(callback1 = callbackHelper(function(elements) { assertEquals(1, elements.length); assertTrue(elements[0] instanceof webdriver.WebElement); - elements[0].toWireValue(). + elements[0].getId_(). then(callback2 = callbackHelper(function(id) { webdriver.test.testutil.assertObjectEquals(json, id); })); @@ -1909,15 +1917,13 @@ function testFindElements_byJs_canPassScriptArguments() { function testSendKeysConvertsVarArgsIntoStrings_simpleArgs() { var testHelper = TestHelper. expectingSuccess(). - expect(CName.FIND_ELEMENT, {'using':'id', 'value':'foo'}). - andReturnSuccess({'ELEMENT':'one'}). expect(CName.SEND_KEYS_TO_ELEMENT, {'id':{'ELEMENT':'one'}, 'value':['1','2','abc','3']}). andReturnSuccess(). replayAll(); var driver = testHelper.createDriver(); - var element = driver.findElement(By.id('foo')); + var element = new webdriver.WebElement(driver, {'ELEMENT': 'one'}); element.sendKeys(1, 2, 'abc', 3); testHelper.execute(); } @@ -1973,16 +1979,17 @@ function testElementEquals_doesNotSendRpcIfElementsHaveSameId() { } function testElementEquals_sendsRpcIfElementsHaveDifferentIds() { + var id1 = {'ELEMENT':'foo'}; + var id2 = {'ELEMENT':'bar'}; var testHelper = TestHelper. expectingSuccess(). - expect(CName.ELEMENT_EQUALS, - {'id':{'ELEMENT':'foo'}, 'other':{'ELEMENT':'bar'}}). + expect(CName.ELEMENT_EQUALS, {'id':id1, 'other':id2}). andReturnSuccess(true). replayAll(); var driver = testHelper.createDriver(); - var a = new webdriver.WebElement(driver, 'foo'), - b = new webdriver.WebElement(driver, 'bar'), + var a = new webdriver.WebElement(driver, id1), + b = new webdriver.WebElement(driver, id2), callback; webdriver.WebElement.equals(a, b).then( @@ -2077,40 +2084,36 @@ function testUnhandledAlertErrors_usesEmptyStringIfAlertTextOmittedFromResponse( } function testAlertHandleResolvesWhenPromisedTextResolves() { - var text = new webdriver.promise.Deferred(); + var promise = new webdriver.promise.Deferred(); - var alert = new webdriver.Alert(STUB_DRIVER, text); + var alert = new webdriver.AlertPromise(STUB_DRIVER, promise); assertTrue(alert.isPending()); - var callback, textCallback; - webdriver.promise.when(alert, - callback = callbackHelper(function(resolvedAlert) { - assertEquals(alert, resolvedAlert); - assertFalse(alert.getText().isPending()); - })); - + var callback; webdriver.promise.when(alert.getText(), - textCallback = callbackHelper(function(text) { + callback = callbackHelper(function(text) { assertEquals('foo', text); })); callback.assertNotCalled(); - textCallback.assertNotCalled(); - text.fulfill('foo'); + promise.fulfill(new webdriver.Alert(STUB_DRIVER, 'foo')); callback.assertCalled(); - textCallback.assertCalled(); verifyAll(); // Make tearDown happy. } function testWebElementsBelongToSameFlowAsParentDriver() { - var testHelper = TestHelper.expectingSuccess().replayAll(); + var testHelper = TestHelper + .expectingSuccess() + .expect(CName.FIND_ELEMENT, {'using':'id', 'value':'foo'}) + .andReturnSuccess({'ELEMENT': 'abc123'}) + .replayAll(); var driver = testHelper.createDriver(); webdriver.promise.createFlow(function() { - new webdriver.WebElement(driver, 'foo').then(function() { + driver.findElement({id: 'foo'}).then(function() { assertEquals( 'WebElement should belong to the same flow as its parent driver', driver.controlFlow(), webdriver.promise.controlFlow()); @@ -2121,12 +2124,29 @@ function testWebElementsBelongToSameFlowAsParentDriver() { } +function testSwitchToAlertThatIsNotPresent() { + var testHelper = TestHelper + .expectingFailure(expectedError(ECode.NO_MODAL_DIALOG_OPEN, 'no alert')) + .expect(CName.GET_ALERT_TEXT) + .andReturnError(ECode.NO_MODAL_DIALOG_OPEN, {'message': 'no alert'}) + .replayAll(); + + var driver = testHelper.createDriver(); + var alert = driver.switchTo().alert(); + alert.dismiss(); // Should never execute. + testHelper.execute(); +} + + function testAlertsBelongToSameFlowAsParentDriver() { - var testHelper = TestHelper.expectingSuccess().replayAll(); + var testHelper = TestHelper + .expectingSuccess() + .expect(CName.GET_ALERT_TEXT).andReturnSuccess('hello') + .replayAll(); var driver = testHelper.createDriver(); webdriver.promise.createFlow(function() { - new webdriver.Alert(driver, 'foo').then(function() { + driver.switchTo().alert().then(function() { assertEquals( 'Alert should belong to the same flow as its parent driver', driver.controlFlow(), webdriver.promise.controlFlow()); diff --git a/javascript/webdriver/webdriver.js b/javascript/webdriver/webdriver.js index 77f69ed8ab0c6..31d6642007215 100644 --- a/javascript/webdriver/webdriver.js +++ b/javascript/webdriver/webdriver.js @@ -17,9 +17,11 @@ */ goog.provide('webdriver.Alert'); +goog.provide('webdriver.AlertPromise'); goog.provide('webdriver.UnhandledAlertError'); goog.provide('webdriver.WebDriver'); goog.provide('webdriver.WebElement'); +goog.provide('webdriver.WebElementPromise'); goog.require('bot.Error'); goog.require('bot.ErrorCode'); @@ -144,9 +146,8 @@ webdriver.WebDriver.acquireSession_ = function(executor, command, description) { * Converts an object to its JSON representation in the WebDriver wire protocol. * When converting values of type object, the following steps will be taken: *

    - *
  1. if the object provides a "toWireValue" function, the return value will - * be returned in its fully resolved state (e.g. this function may return - * promise values)
  2. + *
  3. if the object is a WebElement, the return value will be the element's + * server ID
  4. *
  5. if the object provides a "toJSON" function, the return value of this * function will be returned
  6. *
  7. otherwise, the value of each key will be recursively converted according @@ -154,20 +155,23 @@ webdriver.WebDriver.acquireSession_ = function(executor, command, description) { *
* * @param {*} obj The object to convert. - * @return {!webdriver.promise.Promise} A promise that will resolve to the + * @return {!webdriver.promise.Promise.} A promise that will resolve to the * input value's JSON representation. * @private * @see http://code.google.com/p/selenium/wiki/JsonWireProtocol */ webdriver.WebDriver.toWireValue_ = function(obj) { + if (webdriver.promise.isPromise(obj)) { + return obj.then(webdriver.WebDriver.toWireValue_); + } switch (goog.typeOf(obj)) { case 'array': - return webdriver.promise.fullyResolved( + return webdriver.promise.all( goog.array.map(/** @type {!Array} */ (obj), webdriver.WebDriver.toWireValue_)); case 'object': - if (goog.isFunction(obj.toWireValue)) { - return webdriver.promise.fullyResolved(obj.toWireValue()); + if (obj instanceof webdriver.WebElement) { + return obj.getId_(); } if (goog.isFunction(obj.toJSON)) { return webdriver.promise.fulfilled(obj.toJSON()); @@ -209,8 +213,7 @@ webdriver.WebDriver.fromWireValue_ = function(driver, value) { goog.partial(webdriver.WebDriver.fromWireValue_, driver)); } else if (value && goog.isObject(value) && !goog.isFunction(value)) { if (webdriver.WebElement.ELEMENT_KEY in value) { - value = new webdriver.WebElement(driver, - value[webdriver.WebElement.ELEMENT_KEY]); + value = new webdriver.WebElement(driver, value); } else { value = goog.object.map(/**@type {!Object}*/ (value), goog.partial(webdriver.WebDriver.fromWireValue_, driver)); @@ -230,8 +233,7 @@ webdriver.WebDriver.fromWireValue_ = function(driver, value) { * @private */ webdriver.WebDriver.executeCommand_ = function(executor, command) { - return webdriver.promise.fullyResolved(command.getParameters()). - then(webdriver.WebDriver.toWireValue_). + return webdriver.WebDriver.toWireValue_(command.getParameters()). then(function(parameters) { command.setParameters(parameters); return webdriver.promise.checkedNodeCall( @@ -279,7 +281,7 @@ webdriver.WebDriver.prototype.schedule = function(command, description) { var value = response['value']; if (ex.code === bot.ErrorCode.MODAL_DIALOG_OPENED) { var text = value && value['alert'] ? value['alert']['text'] : ''; - throw new webdriver.UnhandledAlertError(ex.message, + throw new webdriver.UnhandledAlertError(ex.message, text, new webdriver.Alert(self, text)); } throw ex; @@ -527,8 +529,8 @@ webdriver.WebDriver.prototype.call = function(fn, opt_scope, var_args) { * user supplied function. If any errors occur while evaluating the wait, they * will be allowed to propagate. * - *

In the event a condition returns a {@link webdriver.promise.Promise}, the - * polling loop will wait for it to be resolved and use the resolved value for + *

In the event a condition returns a {@link webdriver.promise.Promise}, the + * polling loop will wait for it to be resolved and use the resolved value for * evaluating whether the condition has been satisfied. The resolution time for * a promise is factored into whether a wait has timed out. * @@ -711,7 +713,7 @@ webdriver.WebDriver.prototype.findElement = function(locator) { id = this.schedule(command, 'WebDriver.findElement(' + locator + ')'); } } - return new webdriver.WebElement(this, id); + return new webdriver.WebElementPromise(this, id); }; @@ -1376,13 +1378,13 @@ webdriver.WebDriver.TargetLocator = function(driver) { * Schedules a command retrieve the {@code document.activeElement} element on * the current document, or {@code document.body} if activeElement is not * available. - * @return {!webdriver.WebElement} The active element. + * @return {!webdriver.WebElementPromise} The active element. */ webdriver.WebDriver.TargetLocator.prototype.activeElement = function() { var id = this.driver_.schedule( new webdriver.Command(webdriver.CommandName.GET_ACTIVE_ELEMENT), 'WebDriver.switchTo().activeElement()'); - return new webdriver.WebElement(this.driver_, id); + return new webdriver.WebElementPromise(driver, id); }; @@ -1450,13 +1452,16 @@ webdriver.WebDriver.TargetLocator.prototype.window = function(nameOrHandle) { * Schedules a command to change focus to the active alert dialog. This command * will return a {@link bot.ErrorCode.NO_MODAL_DIALOG_OPEN} error if a modal * dialog is not currently open. - * @return {!webdriver.Alert} The open alert. + * @return {!webdriver.AlertPromise} The open alert. */ webdriver.WebDriver.TargetLocator.prototype.alert = function() { var text = this.driver_.schedule( new webdriver.Command(webdriver.CommandName.GET_ALERT_TEXT), 'WebDriver.switchTo().alert()'); - return new webdriver.Alert(this.driver_, text); + var driver = this.driver_; + return new webdriver.AlertPromise(driver, text.then(function(text) { + return new webdriver.Alert(driver, text); + })); }; @@ -1517,52 +1522,18 @@ webdriver.Key.chord = function(var_args) { * * @param {!webdriver.WebDriver} driver The parent WebDriver instance for this * element. - * @param {!(string|webdriver.promise.Promise)} id Either the opaque ID for the - * underlying DOM element assigned by the server, or a promise that will - * resolve to that ID or another WebElement. + * @param {webdriver.WebElement.Id} id The server-assigned opaque ID for the + * underlying DOM element. * @constructor - * @extends {webdriver.promise.Deferred} */ webdriver.WebElement = function(driver, id) { - webdriver.promise.Deferred.call(this, null, driver.controlFlow()); - /** - * The parent WebDriver instance for this element. - * @private {!webdriver.WebDriver} - */ + /** @private {!webdriver.WebDriver} */ this.driver_ = driver; - // This class is responsible for resolving itself; delete the resolve and - // reject methods so they may not be accessed by consumers of this class. - var fulfill = goog.partial(this.fulfill, this); - var reject = this.reject; - delete this.promise; - delete this.fulfill; - delete this.reject; - - /** - * A promise that resolves to the JSON representation of this WebElement's - * ID, as defined by the WebDriver wire protocol. - * @private {!webdriver.promise.Promise.} - * @see http://code.google.com/p/selenium/wiki/JsonWireProtocol - */ - this.id_ = webdriver.promise.when(id, function(id) { - if (id instanceof webdriver.WebElement) { - return id.id_; - } else if (goog.isDef(id[webdriver.WebElement.ELEMENT_KEY])) { - return id; - } - - var json = {}; - json[webdriver.WebElement.ELEMENT_KEY] = id; - return json; - }); - - // This WebElement should not be resolved until its ID has been - // fully resolved. - this.id_.then(fulfill, reject); + /** @private {!webdriver.promise.Promise.} */ + this.id_ = webdriver.promise.fulfilled(id); }; -goog.inherits(webdriver.WebElement, webdriver.promise.Deferred); /** @@ -1593,7 +1564,8 @@ webdriver.WebElement.equals = function(a, b) { if (a == b) { return webdriver.promise.fulfilled(true); } - return webdriver.promise.fullyResolved([a.id_, b.id_]).then(function(ids) { + var ids = [a.getId_(), b.getId_()]; + return webdriver.promise.all(ids).then(function(ids) { // If the two element's have the same ID, they should be considered // equal. Otherwise, they may still be equivalent, but we'll need to // ask the server to check for us. @@ -1602,10 +1574,10 @@ webdriver.WebElement.equals = function(a, b) { return true; } - var command = new webdriver.Command( - webdriver.CommandName.ELEMENT_EQUALS); - command.setParameter('other', b); - return a.schedule_(command, 'webdriver.WebElement.equals()'); + var command = new webdriver.Command(webdriver.CommandName.ELEMENT_EQUALS); + command.setParameter('id', ids[0]); + command.setParameter('other', ids[1]); + return a.driver_.schedule(command, 'webdriver.WebElement.equals()'); }); }; @@ -1622,9 +1594,10 @@ webdriver.WebElement.prototype.getDriver = function() { * @return {!webdriver.promise.Promise.} A promise * that resolves to this element's JSON representation as defined by the * WebDriver wire protocol. + * @private * @see http://code.google.com/p/selenium/wiki/JsonWireProtocol */ -webdriver.WebElement.prototype.toWireValue = function() { +webdriver.WebElement.prototype.getId_ = function() { return this.id_; }; @@ -1642,7 +1615,7 @@ webdriver.WebElement.prototype.toWireValue = function() { * @private */ webdriver.WebElement.prototype.schedule_ = function(command, description) { - command.setParameter('id', this.id_); + command.setParameter('id', this.getId_()); return this.driver_.schedule(command, description); }; @@ -1699,7 +1672,7 @@ webdriver.WebElement.prototype.findElement = function(locator) { setParameter('value', locator.value); id = this.schedule_(command, 'WebElement.findElement(' + locator + ')'); } - return new webdriver.WebElement(this.driver_, id); + return new webdriver.WebElementPromise(this.driver_, id); }; @@ -2019,6 +1992,61 @@ webdriver.WebElement.prototype.getInnerHtml = function() { +/** + * WebElementPromise is a promise that will be fulfilled with a WebElement. + * This serves as a forward proxy on WebElement, allowing calls to be + * scheduled without directly on this instance before the underlying + * WebElement has been fulfilled. In other words, the following two statements + * are equivalent: + *


+ *     driver.findElement({id: 'my-button'}).click();
+ *     driver.findElement({id: 'my-button'}).then(function(el) {
+ *       return el.click();
+ *     });
+ * 
+ * + * @param {!webdriver.WebDriver} driver The parent WebDriver instance for this + * element. + * @param {!webdriver.promise.Promise.} el A promise + * that will resolve to the promised element. + * @constructor + * @extends {webdriver.WebElement} + * @implements {webdriver.promise.Thenable.} + * @final + */ +webdriver.WebElementPromise = function(driver, el) { + var unused = webdriver.promise.defer(); + webdriver.WebElement.call(this, driver, unused.promise); + + /** @override */ + this.cancel = goog.bind(el.cancel, el); + + /** @override */ + this.isPending = goog.bind(el.isPending, el); + + /** @override */ + this.then = goog.bind(el.then, el); + + /** @override */ + this.thenCatch = goog.bind(el.thenCatch, el); + + /** @override */ + this.thenFinally = goog.bind(el.thenFinally, el); + + /** + * Defers returning the element ID until the wrapped WebElement has been + * resolved. + * @override + */ + this.getId_ = function() { + return el.then(function(el) { + return el.getId_(); + }); + }; +}; +goog.inherits(webdriver.WebElementPromise, webdriver.WebElement); + + /** * Represents a modal dialog such as {@code alert}, {@code confirm}, or * {@code prompt}. Provides functions to retrieve the message displayed with @@ -2026,33 +2054,16 @@ webdriver.WebElement.prototype.getInnerHtml = function() { * case of {@code prompt}). * @param {!webdriver.WebDriver} driver The driver controlling the browser this * alert is attached to. - * @param {!(string|webdriver.promise.Promise.)} text Either the - * message text displayed with this alert, or a promise that will be - * resolved to said text. + * @param {string} text The message text displayed with this alert. * @constructor - * @extends {webdriver.promise.Deferred} */ webdriver.Alert = function(driver, text) { - goog.base(this, null, driver.controlFlow()); - /** @private {!webdriver.WebDriver} */ this.driver_ = driver; - // This class is responsible for resolving itself; delete the resolve and - // reject methods so they may not be accessed by consumers of this class. - var fulfill = goog.partial(this.fulfill, this); - var reject = this.reject; - delete this.promise; - delete this.fulfill; - delete this.reject; - /** @private {!webdriver.promise.Promise.} */ this.text_ = webdriver.promise.when(text); - - // Make sure this instance is resolved when its displayed text is. - this.text_.then(fulfill, reject); }; -goog.inherits(webdriver.Alert, webdriver.promise.Deferred); /** @@ -2107,25 +2118,92 @@ webdriver.Alert.prototype.sendKeys = function(text) { +/** + * AlertPromise is a promise that will be fulfilled with an Alert. This promise + * serves as a forward proxy on an Alert, allowing calls to be scheduled + * directly on this instance before the underlying Alert has been fulfilled. In + * other words, the following two statements are equivalent: + *

+ *     driver.switchTo().alert().dismiss();
+ *     driver.switchTo().alert().then(function(alert) {
+ *       return alert.dismiss();
+ *     });
+ * 
+ * + * @param {!webdriver.WebDriver} driver The driver controlling the browser this + * alert is attached to. + * @param {!webdriver.promise.Thenable.} alert A thenable + * that will be fulfilled with the promised alert. + * @constructor + * @extends {webdriver.Alert} + * @implements {webdriver.promise.Thenable.} + * @final + */ +webdriver.AlertPromise = function(driver, alert) { + webdriver.Alert.call(this, driver, 'unused'); + + /** @override */ + this.cancel = goog.bind(alert.cancel, alert); + + /** @override */ + this.isPending = goog.bind(alert.isPending, alert); + + /** @override */ + this.then = goog.bind(alert.then, alert); + + /** @override */ + this.thenCatch = goog.bind(alert.thenCatch, alert); + + /** @override */ + this.thenFinally = goog.bind(alert.thenFinally, alert); + + /** + * Defer returning text until the promised alert has been resolved. + * @override + */ + this.getText = function() { + return alert.then(function(alert) { + return alert.getText(); + }); + }; +}; +goog.inherits(webdriver.AlertPromise, webdriver.Alert); + + + /** * An error returned to indicate that there is an unhandled modal dialog on the * current page. * @param {string} message The error message. + * @param {string} text The text displayed with the unhandled alert. * @param {!webdriver.Alert} alert The alert handle. * @constructor * @extends {bot.Error} */ -webdriver.UnhandledAlertError = function(message, alert) { +webdriver.UnhandledAlertError = function(message, text, alert) { goog.base(this, bot.ErrorCode.MODAL_DIALOG_OPENED, message); + /** @private {string} */ + this.text_ = text; + /** @private {!webdriver.Alert} */ this.alert_ = alert; }; goog.inherits(webdriver.UnhandledAlertError, bot.Error); +/** + * @return {string} The text displayed with the unhandled alert. + */ +webdriver.UnhandledAlertError.prototype.getAlertText = function() { + return this.text_; +}; + + /** * @return {!webdriver.Alert} The open alert. + * @deprecated Use {@link #getAlertText}. This method will be removed in + * 2.45.0. */ webdriver.UnhandledAlertError.prototype.getAlert = function() { return this.alert_;