Skip to content

Commit 53b0c0f

Browse files
committed
js: Properly track the top of the stack in the ControlFlow to ensure promise
callbacks interrupt the correct frame. Fixes #444.
1 parent 4f86f01 commit 53b0c0f

File tree

3 files changed

+122
-5
lines changed

3 files changed

+122
-5
lines changed

Diff for: javascript/node/selenium-webdriver/CHANGES.md

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
* Removed deprecated enum values: `ErrorCode.NO_MODAL_DIALOG_OPEN` and
1313
`ErrorCode.MODAL_DIALOG_OPENED`. Use `ErrorCode.NO_SUCH_ALERT` and
1414
`ErrorCode.UNEXPECTED_ALERT_OPEN`, respectively.
15+
* FIXED: The `promise.ControlFlow` will maintain state for promise chains
16+
generated in a loop.
1517
* FIXED: Correct serialize target elements used in an action sequence.
1618
* FIXED: `promise.ControlFlow#wait()` now has consistent semantics for an
1719
omitted or 0-timeout: it will wait indefinitely.

Diff for: javascript/webdriver/promise.js

+45-5
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ goog.module('webdriver.promise');
5353
goog.module.declareLegacyNamespace();
5454

5555
var Arrays = goog.require('goog.array');
56+
var asserts = goog.require('goog.asserts');
5657
var asyncRun = goog.require('goog.async.run');
5758
var throwException = goog.require('goog.async.throwException');
5859
var DebugError = goog.require('goog.debug.Error');
@@ -515,7 +516,7 @@ promise.Promise.prototype.scheduleNotifications_ = function() {
515516
}
516517

517518
if (this.callbacks_ && this.callbacks_.length) {
518-
activeFrame = this.flow_.getSchedulingFrame_();
519+
activeFrame = this.flow_.getRunningFrame_();
519520
var self = this;
520521
this.callbacks_.forEach(function(callback) {
521522
if (!callback.frame_.getParent()) {
@@ -1238,6 +1239,17 @@ promise.ControlFlow = function() {
12381239
*/
12391240
this.activeFrame_ = null;
12401241

1242+
/**
1243+
* A reference to the frame which is currently top of the stack in
1244+
* {@link #runInFrame_}. The {@link #activeFrame_} will always be an ancestor
1245+
* of the {@link #runningFrame_}, but the two will often not be the same. The
1246+
* active frame represents which frame is currently executing a task while the
1247+
* running frame represents either the task itself or a promise callback which
1248+
* has fired asynchronously.
1249+
* @private {Frame}
1250+
*/
1251+
this.runningFrame_ = null;
1252+
12411253
/**
12421254
* A reference to the frame in which new tasks should be scheduled. If
12431255
* {@code null}, tasks will be scheduled within the active frame. When forcing
@@ -1368,6 +1380,7 @@ promise.ControlFlow.prototype.reset = function() {
13681380
promise.ControlFlow.prototype.getSchedule = function(opt_includeStackTraces) {
13691381
var ret = 'ControlFlow::' + goog.getUid(this);
13701382
var activeFrame = this.activeFrame_;
1383+
var runningFrame = this.runningFrame_;
13711384
if (!activeFrame) {
13721385
return ret;
13731386
}
@@ -1388,6 +1401,9 @@ promise.ControlFlow.prototype.getSchedule = function(opt_includeStackTraces) {
13881401
if (node === activeFrame) {
13891402
ret = '(active) ' + ret;
13901403
}
1404+
if (node === runningFrame) {
1405+
ret = '(running) ' + ret;
1406+
}
13911407
if (node instanceof Frame) {
13921408
if (node.getPendingTask()) {
13931409
ret += '\n' + toStringHelper(
@@ -1445,6 +1461,15 @@ promise.ControlFlow.prototype.getSchedulingFrame_ = function() {
14451461
};
14461462

14471463

1464+
/**
1465+
* @return {!Frame} The frame that is current executing.
1466+
* @private
1467+
*/
1468+
promise.ControlFlow.prototype.getRunningFrame_ = function() {
1469+
return this.runningFrame_ || this.getActiveFrame_();
1470+
};
1471+
1472+
14481473
/**
14491474
* Schedules a task for execution. If there is nothing currently in the
14501475
* queue, the task will be executed in the next turn of the event loop. If
@@ -1790,22 +1815,27 @@ promise.ControlFlow.prototype.abortFrame_ = function(error, opt_frame) {
17901815
/**
17911816
* Executes a function within a specific frame. If the function does not
17921817
* schedule any new tasks, the frame will be discarded and the function's result
1793-
* returned immediately. Otherwise, a promise will be returned. This promise
1794-
* will be resolved with the function's result once all of the tasks scheduled
1795-
* within the function have been completed. If the function's frame is aborted,
1796-
* the returned promise will be rejected.
1818+
* returned passed to the given callback immediately. Otherwise, the callback
1819+
* will be invoked once all of the tasks scheduled within the function have been
1820+
* completed. If the frame is aborted, the `errback` will be invoked with the
1821+
* offending error.
17971822
*
17981823
* @param {!Frame} newFrame The frame to use.
17991824
* @param {!Function} fn The function to execute.
18001825
* @param {function(T)} callback The function to call with a successful result.
18011826
* @param {function(*)} errback The function to call if there is an error.
18021827
* @param {boolean=} opt_isTask Whether the function is a task and the frame
18031828
* should be immediately activated to capture subtasks and errors.
1829+
* @throws {Error} If this function is invoked while another call to this
1830+
* function is present on the stack.
18041831
* @template T
18051832
* @private
18061833
*/
18071834
promise.ControlFlow.prototype.runInFrame_ = function(
18081835
newFrame, fn, callback, errback, opt_isTask) {
1836+
asserts.assert(
1837+
!this.runningFrame_, 'unexpected recursive call to runInFrame');
1838+
18091839
var self = this,
18101840
oldFrame = this.activeFrame_;
18111841

@@ -1821,12 +1851,19 @@ promise.ControlFlow.prototype.runInFrame_ = function(
18211851
}
18221852

18231853
try {
1854+
this.runningFrame_ = newFrame;
18241855
this.schedulingFrame_ = newFrame;
18251856
activeFlows.push(this);
18261857
var result = fn();
18271858
} finally {
18281859
activeFlows.pop();
18291860
this.schedulingFrame_ = null;
1861+
1862+
// `newFrame` should only be considered the running frame when it is
1863+
// actually executing. After it finishes, set top of stack to its parent
1864+
// so any failures/interrupts that occur while processing newFrame's
1865+
// result are handled there.
1866+
this.runningFrame_ = newFrame.parent_;
18301867
}
18311868
newFrame.isLocked_ = true;
18321869

@@ -1878,6 +1915,9 @@ promise.ControlFlow.prototype.runInFrame_ = function(
18781915
} catch (ex) {
18791916
removeNewFrame(ex);
18801917
errback(ex);
1918+
} finally {
1919+
// No longer running anything, clear the reference.
1920+
this.runningFrame_ = null;
18811921
}
18821922

18831923
function isCloseable(frame) {

Diff for: javascript/webdriver/test/promise_flow_test.js

+75
Original file line numberDiff line numberDiff line change
@@ -1880,3 +1880,78 @@ function testDoesNotModifyRejectionErrorIfPromiseNotInsideAFlow() {
18801880
webdriver.promise.rejected(error).then(pair.callback, pair.errback);
18811881
return waitForIdle().then(pair.assertErrback);
18821882
}
1883+
1884+
1885+
/** See https://github.com/SeleniumHQ/selenium/issues/444 */
1886+
function testMaintainsOrderWithPromiseChainsCreatedWithinAForeach_1() {
1887+
var messages = [];
1888+
flow.execute(function() {
1889+
return webdriver.promise.fulfilled(['a', 'b', 'c', 'd']);
1890+
}, 'start').then(function(steps) {
1891+
steps.forEach(function(step) {
1892+
webdriver.promise.fulfilled(step)
1893+
.then(function() {
1894+
messages.push(step + '.1');
1895+
}).then(function() {
1896+
messages.push(step + '.2');
1897+
});
1898+
})
1899+
});
1900+
return waitForIdle().then(function() {
1901+
assertArrayEquals(
1902+
['a.1', 'b.1', 'c.1', 'd.1', 'a.2', 'b.2', 'c.2', 'd.2'],
1903+
messages);
1904+
});
1905+
}
1906+
1907+
1908+
/** See https://github.com/SeleniumHQ/selenium/issues/444 */
1909+
function testMaintainsOrderWithPromiseChainsCreatedWithinAForeach_2() {
1910+
var messages = [];
1911+
flow.execute(function() {
1912+
return webdriver.promise.fulfilled(['a', 'b', 'c', 'd']);
1913+
}, 'start').then(function(steps) {
1914+
steps.forEach(function(step) {
1915+
webdriver.promise.fulfilled(step)
1916+
.then(function() {
1917+
messages.push(step + '.1');
1918+
}).then(function() {
1919+
flow.execute(function() {}, step + '.2').then(function(text) {
1920+
messages.push(step + '.2');
1921+
});
1922+
});
1923+
})
1924+
});
1925+
return waitForIdle().then(function() {
1926+
assertArrayEquals(
1927+
['a.1', 'b.1', 'c.1', 'd.1', 'a.2', 'b.2', 'c.2', 'd.2'],
1928+
messages);
1929+
});
1930+
}
1931+
1932+
1933+
/** See https://github.com/SeleniumHQ/selenium/issues/444 */
1934+
function testMaintainsOrderWithPromiseChainsCreatedWithinAForeach_3() {
1935+
var messages = [];
1936+
flow.execute(function() {
1937+
return webdriver.promise.fulfilled(['a', 'b', 'c', 'd']);
1938+
}, 'start').then(function(steps) {
1939+
steps.forEach(function(step) {
1940+
webdriver.promise.fulfilled(step)
1941+
.then(function(){})
1942+
.then(function() {
1943+
messages.push(step + '.1');
1944+
return flow.execute(function() {}, step + '.1');
1945+
}).then(function() {
1946+
flow.execute(function() {}, step + '.2').then(function(text) {
1947+
messages.push(step + '.2');
1948+
});
1949+
});
1950+
})
1951+
});
1952+
return waitForIdle().then(function() {
1953+
assertArrayEquals(
1954+
['a.1', 'b.1', 'c.1', 'd.1', 'a.2', 'b.2', 'c.2', 'd.2'],
1955+
messages);
1956+
});
1957+
}

0 commit comments

Comments
 (0)