Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate and remove the WebDriverJS promise manager #2969

Closed
jleyba opened this issue Oct 19, 2016 · 48 comments
Closed

Deprecate and remove the WebDriverJS promise manager #2969

jleyba opened this issue Oct 19, 2016 · 48 comments
Assignees
Labels

Comments

@jleyba
Copy link
Contributor

jleyba commented Oct 19, 2016

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)

  1. Within selenium-webdriver, replace all hard dependencies on the selenium-webdriver/promise.ControlFlow class with a new interface: selenium-webdriver/promise.Scheduler.

    /** @interface */
    class Scheduler {
     execute(fn) {}
     timeout(ms) {}
     wait(condition, opt_timeout) {}
    }
  2. 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
     }
    }
  3. Introduce the SELENIUM_PROMISE_MANAGER environment variable. When set to 1, selenium-webdriver will use the existing ControlFlow scheduler. When set to 0, the SimpleScheduler will be used.

    When SELENIUM_PROMISE_MANAGER=0, any attempts to use the ControlFlow class will trigger an error. This will help users catch any unexpected direct dependencies. This will also impact the use of the promise.Deferred and promise.Promise classes, as well as the promise.fulfilled() and promise.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.

@mekdev
Copy link

mekdev commented Nov 1, 2016

@jleyba I am happy to help out with documentation for this.
We just swapped out the promise manager to async/await entirely with babel and its working well for us!

For other people following up:

There is some sample code we put on github for reference using Mocha and Async / Await
https://github.com/airware/webdriver-mocha-async-await-example

The technical talk we gave at SF Selenium Meetup in Oct 2016 on the move to Async/Await can be found here: Fullstack End-to-end test automation with Node.js, one year later

jleyba added a commit that referenced this issue Nov 2, 2016
…ch can be

set to 0 to disable use of the promise manager.

Full details and motivation behind this change can be found in #2969
jleyba added a commit that referenced this issue Nov 2, 2016
…an issue

commands directly, or through a standard promise callback. This is the same
pattern used for WebElements with WebDriver.findElement().

Removed Builder.buildAsync() as it was redundant with the change above.

This change is part of #2969, to support awaiting the build result:

async function demo(url); {
  let driver = await new Builder().forBrowser('firefox').build();
  return driver.get(�url);
}
jleyba added a commit that referenced this issue Nov 2, 2016
Using the property setter will always take precedence over the environment
variable.

#2969
@jleyba
Copy link
Contributor Author

jleyba commented Nov 3, 2016

Phase 1 is complete: 3.0.0 has been published to npm.

@wcdeich4
Copy link

wcdeich4 commented Feb 3, 2017

After the promise manager is removed, how will people be able to call WebDriver functions in NodeJS imperatively (one after another)?

@kidmillions
Copy link

kidmillions commented Feb 3, 2017

@jleyba
Copy link
Contributor Author

jleyba commented Feb 3, 2017

There's an example of using async/await in the background section of the initial write up for this issue.

@wcdeich4
Copy link

wcdeich4 commented Feb 3, 2017

Ok. I tried this, but I still get an error:

driver = new webdriver.Builder().forBrowser('chrome').build();
await driver.get(url);

await driver.get(url);
^^^^^^
SyntaxError: Unexpected identifier

driver = await new webdriver.Builder().forBrowser('chrome').build(); has a similar exception

await driver.manage().window().maximize(); has a similar exception.

process.env.SELENIUM_PROMISE_MANAGER=0; did not stop the errors

@mekdev
Copy link

mekdev commented Feb 3, 2017

@wcdeich4

What node version are you using ? async await is supported in Node 7 and above with a feature flag. Else you can use babel to transpile to older versions of Node.

@mekdev
Copy link

mekdev commented Feb 3, 2017

Also for people jumping on board async await, this thread in the Nodejs project tracks the implementation roll out.

nodejs/promises#4

cwolfes pushed a commit to cloudogu/jenkins that referenced this issue Feb 28, 2018
the selenium driver now returns a promise instead of doing some crazy
stack-magic.

see this issue for more information:
SeleniumHQ/selenium#2969
cwolfes pushed a commit to cloudogu/redmine that referenced this issue Feb 28, 2018
the selenium driver now returns a promise instead of doing some crazy
stack-magic.

see this issue for more information:
SeleniumHQ/selenium#2969
@johnjbarton
Copy link
Contributor

Since this is a breaking change that impacts almost every line of test code, what is the time line for a complete supported version 4.0 npm module publication?

In other words how much time do we have to prepare?

@jleyba
Copy link
Contributor Author

jleyba commented May 4, 2018

npm already has 4.0.0-alpha.1 which includes the removal of the promise manager. It's tagged as "alpha" because the Selenium project as a whole wasn't ready to move to 4.0 and I couldn't release it as part of 3.X given the scope of changes. It's still considered a fully supported release, just like anything else published to npm

@awarecan
Copy link
Contributor

awarecan commented May 4, 2018

@jleyba any plan to do another 3.x release?

@johnjbarton
Copy link
Contributor

@jleyba Thanks! Sorry I was not clear. I need to know when selenium-webdriver will release 4.0.

We support multiple languages and we would really prefer to use the same version across all of them. Thus we can't switch to 4.0 alpha, and yet we need a long lead time to prepare for it.

I'm only looking for a ballpark figure, "in the next few weeks", "early fall 2018".

@jimevans
Copy link
Member

jimevans commented May 5, 2018

@johnjbarton Simon Stewart (@shs96c) is on record that we’ll be starting the engines on a 4.0 release once the W3C WebDriver Specification goes to Recommendation, which is likely to happen in late May/early June. Expect some beta period, then a full 4.0 release. In other words, “before Christmas” (inside joke for long-time project followers).

@shanlashari
Copy link

Will we able to use Jasmine after this update implementation ?

@StanislavKharchenko
Copy link

Hello.
What is the planned dates to release of this feature?
Does it mean that all protractor tests need to have async/await in specs and page-objects when promise manager will be completely removed?

Thank you in advance.

@StanislavKharchenko
Copy link

@jleyba How we can use async/await for cases when need to fill element (or block of elements) in web page using setter? Example:

mainPage.Information = "Address information..."

Where information is setter with sendKeys, like:

set Information(value) {
   this.information.sendKeys(value);
}

Typescript is not allow async/await for setters.
So, how we can use such approach without promise manager?

Thank you.

@StanislavKharchenko
Copy link

Does anyone help me with comment above?
What is the destiny with promise manager?

It is a not good situation, when something couldn't work due to deprecation of functionality.
Using a setter to fill web-elements is very laconic style and it is bad to lose it.

Thanks!

@johnjbarton
Copy link
Contributor

await mainPage.setInformation("Address information...");

@StanislavKharchenko
Copy link

StanislavKharchenko commented Jul 24, 2018

@johnjbarton, thank you, but it is not a solution. Its just a method, but the idea to use a setter.

@jleyba
Copy link
Contributor Author

jleyba commented Jul 24, 2018

If TypeScript won't let you use async/await with a setter, don't use a setter.

FWIW, I don't think you should try to hide expensive RPCs in a setter anyway. Bit of an anti-pattern IMO.

@pauldraper
Copy link

@StanislavKharchenko this isn't a TypeScript thing. This is a JavaScript thing.

https://es5.github.io/#x11.13.1

The value of a setter expression is always the RHS.

So there can be no promise to wait on. await page.value = 'abc' makes no sense.

@lock lock bot locked and limited conversation to collaborators Aug 14, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests