Skip to content

Commit f473be4

Browse files
committed
Change ControlFlow.wait() to accept promises
This makes it possible to block the control flow on the resolution of an arbitrary promise (with optional timeout). Fixes issue 8448.
1 parent 010617f commit f473be4

File tree

4 files changed

+145
-18
lines changed

4 files changed

+145
-18
lines changed

javascript/node/selenium-webdriver/CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
`promise.ControlFlow#clearHistory`, and `promise.ControlFlow#annotateError`.
5454
These functions were all intended for internal use and are no longer
5555
necessary, so they have been made no-ops.
56+
* `WebDriver.wait()` may now be used to wait for a promise to resolve, with
57+
an optional timeout. Refer to the API documentation for more information.
5658
* FIXED: 8380: `firefox.Driver` will delete its temporary profile on `quit`.
5759
* FIXED: 8306: Stack overflow in promise callbacks eliminated.
5860
* FIXED: 8221: Added support for defining custom command mappings. Includes

javascript/webdriver/promise.js

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1533,17 +1533,63 @@ promise.ControlFlow.prototype.timeout = function(ms, opt_description) {
15331533
* <p>If the condition function throws, or returns a rejected promise, the
15341534
* wait task will fail.
15351535
*
1536-
* @param {function(): T} condition The condition function to poll.
1537-
* @param {number} timeout How long to wait, in milliseconds, for the condition
1538-
* to hold before timing out.
1536+
* <p>If the condition is defined as a promise, the flow will block on that
1537+
* promise's resolution, up to {@code timeout} milliseconds. If
1538+
* {@code timeout === 0}, the flow will block indefinitely on the promise's
1539+
* resolution.
1540+
*
1541+
* @param {(!promise.Promise<T>|function())} condition The condition to poll,
1542+
* or a promise to wait on.
1543+
* @param {number=} opt_timeout How long to wait, in milliseconds, for the
1544+
* condition to hold before timing out; defaults to 0.
15391545
* @param {string=} opt_message An optional error message to include if the
15401546
* wait times out; defaults to the empty string.
15411547
* @return {!promise.Promise<T>} A promise that will be fulfilled
15421548
* when the condition has been satisified. The promise shall be rejected if
15431549
* the wait times out waiting for the condition.
1550+
* @throws {TypeError} If condition is not a function or promise or if timeout
1551+
* is not a number >= 0.
15441552
* @template T
15451553
*/
1546-
promise.ControlFlow.prototype.wait = function(condition, timeout, opt_message) {
1554+
promise.ControlFlow.prototype.wait = function(
1555+
condition, opt_timeout, opt_message) {
1556+
var timeout = opt_timeout || 0;
1557+
if (!goog.isNumber(timeout) || timeout < 0) {
1558+
throw TypeError('timeout must be a number >= 0: ' + timeout);
1559+
}
1560+
1561+
if (promise.isPromise(condition)) {
1562+
return this.execute(function() {
1563+
if (!timeout) {
1564+
return condition;
1565+
}
1566+
return new promise.Promise(function(fulfill, reject) {
1567+
var start = goog.now();
1568+
var timer = setTimeout(function() {
1569+
timer = null;
1570+
reject(Error((opt_message ? opt_message + '\n' : '') +
1571+
'Timed out waiting for promise to resolve after ' +
1572+
(goog.now() - start) + 'ms'));
1573+
}, timeout);
1574+
1575+
/** @type {Thenable} */(condition).then(
1576+
function(value) {
1577+
timer && clearTimeout(timer);
1578+
fulfill(value);
1579+
},
1580+
function(error) {
1581+
timer && clearTimeout(timer);
1582+
reject(error);
1583+
});
1584+
});
1585+
}, opt_message || '<anonymous wait: promise resolution>');
1586+
}
1587+
1588+
if (!goog.isFunction(condition)) {
1589+
throw TypeError('Invalid condition; must be a function or promise: ' +
1590+
goog.typeOf(condition));
1591+
}
1592+
15471593
if (promise.isGenerator(condition)) {
15481594
condition = goog.partial(promise.consume, condition);
15491595
}
@@ -1557,7 +1603,7 @@ promise.ControlFlow.prototype.wait = function(condition, timeout, opt_message) {
15571603

15581604
function pollCondition() {
15591605
self.resume_();
1560-
self.execute(condition).then(function(value) {
1606+
self.execute(/**@type {function()}*/(condition)).then(function(value) {
15611607
var elapsed = goog.now() - startTime;
15621608
if (!!value) {
15631609
fulfill(value);
@@ -1584,6 +1630,7 @@ promise.ControlFlow.prototype.wait = function(condition, timeout, opt_message) {
15841630
* @param {!promise.Promise} promise The promise to wait on.
15851631
* @return {!promise.Promise} A promise that will resolve when the
15861632
* task has completed.
1633+
* @deprecated Use {@link #wait() wait(promise)} instead.
15871634
*/
15881635
promise.ControlFlow.prototype.await = function(promise) {
15891636
return this.execute(function() {

javascript/webdriver/test/promise_flow_test.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1149,7 +1149,53 @@ function testWaiting_scheduleWithIntermittentAndNestedWaits() {
11491149
'0: wait 4');
11501150
});
11511151
}
1152-
1152+
1153+
1154+
function testWait_requiresConditionToBeAPromiseOrFunction() {
1155+
assertThrows(function() {
1156+
flow.wait(1234, 0);
1157+
});
1158+
flow.wait(function() { return true;}, 0);
1159+
flow.wait(webdriver.promise.fulfilled(), 0);
1160+
return waitForIdle();
1161+
}
1162+
1163+
1164+
function testWait_promiseThatDoesNotResolveBeforeTimeout() {
1165+
var d = webdriver.promise.defer();
1166+
flow.wait(d.promise, 5).then(fail, function(e) {
1167+
assertRegExp(/Timed out waiting for promise to resolve after \d+ms/,
1168+
e.message);
1169+
});
1170+
return waitForIdle().then(function() {
1171+
assertTrue('Promise should not be cancelled', d.promise.isPending());
1172+
});
1173+
}
1174+
1175+
1176+
function testWait_unboundedWaitOnPromiseResolution() {
1177+
var messages = [];
1178+
var d = webdriver.promise.defer();
1179+
var waitResult = flow.wait(d.promise).then(function(value) {
1180+
messages.push('b');
1181+
assertEquals(1234, value);
1182+
});
1183+
setTimeout(function() {
1184+
messages.push('a');
1185+
}, 5);
1186+
1187+
webdriver.promise.delayed(10).then(function() {
1188+
assertArrayEquals(['a'], messages);
1189+
assertTrue(waitResult.isPending());
1190+
d.fulfill(1234);
1191+
return waitResult;
1192+
}).then(function(value) {
1193+
assertArrayEquals(['a', 'b'], messages);
1194+
});
1195+
1196+
return waitForIdle();
1197+
}
1198+
11531199

11541200
function testSubtasks() {
11551201
schedule('a');
@@ -1333,6 +1379,7 @@ function testEventLoopWaitsOnPendingPromiseRejections_multipleRejections() {
13331379
}
13341380
});
13351381
}).then(function() {
1382+
seen.sort();
13361383
assertArrayEquals([once, twice], seen);
13371384
assertFlowHistory('one');
13381385
assertFalse('Did not cancel the second task', twoResult.isPending());

javascript/webdriver/webdriver.js

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -624,28 +624,59 @@ webdriver.WebDriver.prototype.call = function(fn, opt_scope, var_args) {
624624

625625

626626
/**
627-
* Schedules a command to wait for a condition to hold, as defined by some
628-
* user supplied function. If any errors occur while evaluating the wait, they
629-
* will be allowed to propagate.
627+
* Schedules a command to wait for a condition to hold. The condition may be
628+
* specified by a {@link webdriver.until.Condition}, as a custom function, or
629+
* as a {@link webdriver.promise.Promise}.
630630
*
631-
* <p>In the event a condition returns a {@link webdriver.promise.Promise}, the
631+
* For a {@link webdriver.until.Condition} or function, the wait will repeatedly
632+
* evaluate the condition until it returns a truthy value. If any errors occur
633+
* while evaluating the condition, they will be allowed to propagate. In the
634+
* event a condition returns a {@link webdriver.promise.Promise promise}, the
632635
* polling loop will wait for it to be resolved and use the resolved value for
633-
* evaluating whether the condition has been satisfied. The resolution time for
636+
* whether the condition has been satisified. Note the resolution time for
634637
* a promise is factored into whether a wait has timed out.
635638
*
636-
* @param {!(webdriver.until.Condition.<T>|
637-
* function(!webdriver.WebDriver): T)} condition Either a condition
638-
* object, or a function to evaluate as a condition.
639-
* @param {number} timeout How long to wait for the condition to be true.
639+
* *Example:* waiting up to 10 seconds for an element to be present and visible
640+
* on the page.
641+
*
642+
* var button = driver.wait(until.elementLocated(By.id('foo'), 10000);
643+
* button.click();
644+
*
645+
* This function may also be used to block the command flow on the resolution
646+
* of a {@link webdriver.promise.Promise promise}. When given a promise, the
647+
* command will simply wait for its resolution before completing. A timeout may
648+
* be provided to fail the command if the promise does not resolve before the
649+
* timeout expires.
650+
*
651+
* *Example:* Suppose you have a function, `startTestServer`, that returns a
652+
* promise for when a server is ready for requests. You can block a `WebDriver`
653+
* client on this promise with:
654+
*
655+
* var started = startTestServer();
656+
* driver.wait(started, 5 * 1000, 'Server should start within 5 seconds');
657+
* driver.get(getServerUrl());
658+
*
659+
* @param {!(webdriver.promise.Promise<T>|
660+
* webdriver.until.Condition<T>|
661+
* function(!webdriver.WebDriver): T)} condition The condition to
662+
* wait on, defined as a promise, condition object, or a function to
663+
* evaluate as a condition.
664+
* @param {number=} opt_timeout How long to wait for the condition to be true.
640665
* @param {string=} opt_message An optional message to use if the wait times
641666
* out.
642-
* @return {!webdriver.promise.Promise.<T>} A promise that will be fulfilled
667+
* @return {!webdriver.promise.Promise<T>} A promise that will be fulfilled
643668
* with the first truthy value returned by the condition function, or
644669
* rejected if the condition times out.
645670
* @template T
646671
*/
647672
webdriver.WebDriver.prototype.wait = function(
648-
condition, timeout, opt_message) {
673+
condition, opt_timeout, opt_message) {
674+
if (webdriver.promise.isPromise(condition)) {
675+
return this.flow_.wait(
676+
/** @type {!webdriver.promise.Promise<T>} */(condition),
677+
opt_timeout, opt_message);
678+
}
679+
649680
var message = opt_message;
650681
var fn = /** @type {!Function} */(condition);
651682
if (condition instanceof webdriver.until.Condition) {
@@ -659,7 +690,7 @@ webdriver.WebDriver.prototype.wait = function(
659690
return webdriver.promise.consume(fn, null, [driver]);
660691
}
661692
return fn(driver);
662-
}, timeout, message);
693+
}, opt_timeout, message);
663694
};
664695

665696

0 commit comments

Comments
 (0)