Skip to content

Commit

Permalink
Add promise support for generator functions.
Browse files Browse the repository at this point in the history
Requires using harmony (node v0.11.x)
  • Loading branch information
jleyba committed Jul 18, 2014
1 parent 6bcdcfa commit 00d145a
Show file tree
Hide file tree
Showing 10 changed files with 625 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public boolean accept(File dir, String name) {
}

private static boolean hasHtmlSibling(File file) {
File sibling = new File(Files.getNameWithoutExtension(file.getAbsolutePath()) + ".html");
File sibling = new File(file.getParentFile(),
Files.getNameWithoutExtension(file.getAbsolutePath()) + ".html");
return sibling.exists();
}
}
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

* Added support for generator functions to `ControlFlow#execute` and
`ControlFlow#wait`. For more information, see documentation on
`webdriver.promise.consume`. Requires harmony support (run with
`node --harmony-generators` in `v0.11.x`).
* Promise A+ compliance: a promise may no longer resolve to itself.
* For consistency with other language bindings, deprecated
`UnhandledAlertError#getAlert` and added `#getAlertText`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2013 Selenium committers
// Copyright 2013 Software Freedom Conservancy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* @fileoverview An example WebDriver script using Harmony generator functions.
* This requires node v0.11 or newer.
*
* Usage: node --harmony-generators \
* selenium-webdriver/example/google_search_generator.js
*/

var webdriver = require('..');

var driver = new webdriver.Builder().
withCapabilities(webdriver.Capabilities.chrome()).
build();

driver.get('http://www.google.com');
driver.call(function* () {
var query = yield driver.findElement(webdriver.By.name('q'));
query.sendKeys('webdriver');

var submit = yield driver.findElement(webdriver.By.name('btnG'));
submit.click();
});

driver.wait(function* () {
var title = yield driver.getTitle();
return 'webdriver - Google Search' === title;
}, 1000);

driver.quit();
24 changes: 24 additions & 0 deletions javascript/node/selenium-webdriver/test/_base_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,28 @@ describe('Context', function() {
});


function haveGenerators() {
try {
// Test for generator support.
new Function('function* x() {}');
return true;
} catch (ex) {
return false;
}
}


function runClosureTest(file) {
var name = path.basename(file);
name = name.substring(0, name.length - '.js'.length);

// Generator tests will fail to parse in ES5, so mark those tests as
// pending under ES5.
if (name.indexOf('_generator_') != -1 && !haveGenerators()) {
it(name);
return;
}

describe(name, function() {
var context = new base.Context(true);
context.closure.document.title = name;
Expand All @@ -52,8 +70,14 @@ function runClosureTest(file) {
tc.autoDiscoverTests();
}

var shouldRunTests = tc.shouldRunTests();
var allTests = tc.getTests();
allTests.forEach(function(test) {
if (!shouldRunTests) {
it(test.name);
return;
}

it(test.name, function(done) {
tc.setTests([test]);
tc.setCompletedCallback(function() {
Expand Down
107 changes: 106 additions & 1 deletion javascript/webdriver/promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -1302,7 +1302,9 @@ webdriver.promise.ControlFlow.prototype.getSchedule = function() {

/**
* Schedules a task for execution. If there is nothing currently in the
* queue, the task will be executed in the next turn of the event loop.
* queue, the task will be executed in the next turn of the event loop. If
* the task function is a generator, the task will be executed using
* {@link webdriver.promise.consume}.
*
* @param {function(): (T|webdriver.promise.Promise.<T>)} fn The function to
* call to start the task. If the function returns a
Expand All @@ -1315,6 +1317,10 @@ webdriver.promise.ControlFlow.prototype.getSchedule = function() {
*/
webdriver.promise.ControlFlow.prototype.execute = function(
fn, opt_description) {
if (webdriver.promise.isGenerator(fn)) {
fn = goog.partial(webdriver.promise.consume, fn);
}

this.cancelShutdown_();

if (!this.activeFrame_) {
Expand Down Expand Up @@ -1382,6 +1388,10 @@ webdriver.promise.ControlFlow.prototype.wait = function(
var sleep = Math.min(timeout, 100);
var self = this;

if (webdriver.promise.isGenerator(condition)) {
condition = goog.partial(webdriver.promise.consume, condition);
}

return this.execute(function() {
var startTime = goog.now();
var waitResult = new webdriver.promise.Deferred();
Expand Down Expand Up @@ -2145,3 +2155,98 @@ webdriver.promise.createFlow = function(callback) {
return callback(flow);
});
};


/**
* Tests is a function is a generator.
* @param {!Function} fn The function to test.
* @return {boolean} Whether the function is a generator.
*/
webdriver.promise.isGenerator = function(fn) {
return fn.constructor.name === 'GeneratorFunction';
};


/**
* Consumes a {@code GeneratorFunction}. Each time the generator yields a
* promise, this function will wait for it to be fulfilled before feeding the
* fulfilled value back into {@code next}. Likewise, if a yielded promise is
* rejected, the rejection error will be passed to {@code throw}.
*
* <p>Example 1: the Fibonacci Sequence.
* <pre><code>
* webdriver.promise.consume(function* fibonacci() {
* var n1 = 1, n2 = 1;
* for (var i = 0; i < 4; ++i) {
* var tmp = yield n1 + n2;
* n1 = n2;
* n2 = tmp;
* }
* return n1 + n2;
* }).then(function(result) {
* console.log(result); // 13
* });
* </code></pre>
*
* <p>Example 2: a generator that throws.
* <pre><code>
* webdriver.promise.consume(function* () {
* yield webdriver.promise.delayed(250).then(function() {
* throw Error('boom');
* });
* }).thenCatch(function(e) {
* console.log(e.toString()); // Error: boom
* });
* </code></pre>
*
* @param {!Function} generatorFn The generator function to execute.
* @param {Object=} opt_self The object to use as "this" when invoking the
* initial generator.
* @param {...*} var_args Any arguments to pass to the initial generator.
* @return {!webdriver.promise.Promise.<?>} A promise that will resolve to the
* generator's final result.
* @throws {TypeError} If the given function is not a generator.
*/
webdriver.promise.consume = function(generatorFn, opt_self, var_args) {
if (!webdriver.promise.isGenerator(generatorFn)) {
throw TypeError('Input is not a GeneratorFunction: ' +
generatorFn.constructor.name);
}

var deferred = webdriver.promise.defer();
var generator = generatorFn.apply(opt_self, goog.array.slice(arguments, 2));
callNext();
return deferred.promise;

/** @param {*=} opt_value . */
function callNext(opt_value) {
pump(generator.next, opt_value);
}

/** @param {*=} opt_error . */
function callThrow(opt_error) {
// Dictionary lookup required because Closure compiler's built-in
// externs does not include GeneratorFunction.prototype.throw.
pump(generator['throw'], opt_error);
}

function pump(fn, opt_arg) {
if (!deferred.isPending()) {
return; // Defererd was cancelled; silently abort.
}

try {
var result = fn.call(generator, opt_arg);
} catch (ex) {
deferred.reject(ex);
return;
}

if (result.done) {
deferred.fulfill(result.value);
return;
}

webdriver.promise.asap(result.value, callNext, callThrow);
}
};
26 changes: 26 additions & 0 deletions javascript/webdriver/test/promise_generator_test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<script src="test_bootstrap.js"></script>
<script>
goog.require('goog.testing.jsunit');
</script>
<script>
function shouldRunTests() {
try {
new Function('function* x() {}');
return true;
} catch (ex) {
return false;
}
}

// Don't even load the test file if the current browser does not support
// generators as the file will fail to parse. In these cases, we need to
// manually bootstrap Closure's JSUnit framework to make sure the WebDriver
// runner is able to collect results.
if (!shouldRunTests()) {
G_testRunner.setStrict(false);
} else {
goog.require('webdriver.test.promise.generator.test');
}
</script>
<body></body>
Loading

0 comments on commit 00d145a

Please sign in to comment.