Deprecate and remove the WebDriverJS promise manager · Issue #2969 · SeleniumHQ/selenium (original) (raw)
This is a tracking bug for deprecating and removing the promise manager used by WebDriverJS.
Background
WebDriverJS is an asynchronous API, where every command returns a promise. This can make even the simplest WebDriver scripts very verbose. Consider the canonical "Google search" test:
let driver = new Builder().forBrowser('firefox').build(); driver.get('http://www.google.com/ncr') .then(_ => driver.findElement(By.name('q'))) .then(q => q.sendKeys('webdriver')) .then(_ => driver.findElement(By.name('btnG'))) .then(btnG => btnG.click()) .then(_ => driver.wait(until.titleIs('webdriver - Google Search'), 1000)) .then(_ => driver.quit(), e => { console.error(e); driver.quit(); });
WebDriverJS uses a promise manager that tracks command execution, allowing tests to be written as if they were using a synchronous API:
let driver = new Builder().forBrowser('firefox').build(); driver.get('http://www.google.com/ncr'); driver.findElement(By.name('q')).sendKeys('webdriver'); driver.findElement(By.name('btnG')).click(); driver.wait(until.titleIs('webdriver - Google Search'), 1000); driver.quit();
The original goal for the promise manager was to make it easier to read and reason about a test's intent. Unfortunately, any benefits on this front come at the cost of increased implementation complexity and reduced debuggability—in the example above, inserting a break point before the first findElement
call will break before the findElement
command is scheduled. To break before the command actually executes, you would have to modify the test to break in a callback to the driver.get()
command:
let driver = new Builder().forBrowser('firefox').build(); driver.get('http://www.google.com/ncr').then(_ => debugger); driver.findElement(By.name('q')).sendKeys('webdriver');
To help with debugging, tests can be written with generators and WebDriver's promise.consume
function (or equivalent, from libraries like task.js). These libraries are "promise aware": if a generator yields a promise, the library will defer calling next()
until the promise has resolved (if the promise is rejected, the generator's throw()
function will be called with the rejection reason). While users will have to make liberal use of the yield
keyword, they can use this strategy to write "synchronous" tests with predictable breakpoints.
const {Builder, By, promise, until} = require('selenium-webdriver');
let result = promise.consume(function* doGoogleSearch() { let driver = new Builder().forBrowser('firefox').build(); yield driver.get('http://www.google.com/ncr'); yield driver.findElement(By.name('q')).sendKeys('webdriver'); yield driver.findElement(By.name('btnG')).click(); yield driver.wait(until.titleIs('webdriver - Google Search'), 1000); yield driver.quit(); });
result.then(_ => console.log('SUCCESS!'), e => console.error('FAILURE: ' + e));
The need to use promise.consume
/task.js will be eliminated in TC39 with the introduction of async functions:
async function doGoogleSearch() { let driver = new Builder().forBrowser('firefox').build(); await driver.get('http://www.google.com/ncr'); await driver.findElement(By.name('q')).sendKeys('webdriver'); await driver.findElement(By.name('btnG')).click(); await driver.wait(until.titleIs('webdriver - Google Search'), 1000); await driver.quit(); }
doGoogleSearch() .then(_ => console.log('SUCCESS!'), e => console.error('FAILURE: ' + e));
Given that the JavaScript language continues to evolve with more support for asynchronous programming (and promises in particular), the benefits to WebDriver's promise manager no longer outweigh the costs. Therefore, we will be deprecating the promise manager and eventually removing it in favor of native language constructs.
Deprecation Plan
Phase 1: allow users to opt-out of the promise manager
Estimated date: now (selenium-webdriver@3.0)
- Within
selenium-webdriver
, replace all hard dependencies on theselenium-webdriver/promise.ControlFlow
class with a new interface:selenium-webdriver/promise.Scheduler
.
/** @interface */
class Scheduler {
execute(fn) {}
timeout(ms) {}
wait(condition, opt_timeout) {}
} - Add a Scheduler implementation that executes everything immediately using native Promises.
/** @implements {Scheduler} /
class SimpleScheduler {
/* @override / execute(fn) {
return new Promise((resolve, reject) => {
try {
resolve(fn());
} catch (ex) {
reject(ex);
}
});
}
/* @override / timeout(ms) {
return new Promise(resolve => setTimeout(_ => resolve(), ms));
}
/* @override */ wait(condition, opt_timeout) {
// Implementation omitted for brevity
}
} - Introduce the
SELENIUM_PROMISE_MANAGER
environment variable. When set to 1,selenium-webdriver
will use the existingControlFlow
scheduler. When set to 0, theSimpleScheduler
will be used.
WhenSELENIUM_PROMISE_MANAGER=0
, any attempts to use theControlFlow
class will trigger an error. This will help users catch any unexpected direct dependencies. This will also impact the use of thepromise.Deferred
andpromise.Promise
classes, as well as thepromise.fulfilled()
andpromise.rejected()
functions, which all have a hard dependency on the control flow. Users should use the native equivalents.
At this point,SELENIUM_PROMISE_MANAGER
will default to 1, preserving existing functionality.
Phase 2: opt-in to the promise manager
Estimated date: October 2017, Node 8.0
Following the release of a Node LTS that contains async functions, change the SELENIUM_PROMISE_MANAGER
default value to 0. Users must explicitly set this environment variable to continue using the ControlFlow scheduler.
Phase 3: removing the promise manager
Estimated date: October 2018, Node 10.0
On the release of the second major Node LTS with async functions, the ControlFlow class and all related code will be removed. The SELENIUM_PROMISE_MANAGER
environment variable will no longer have any effect.