WebdriverIO supports Chaining without multiple await statements
Tags: automation, javascript, promise, wdio
Published: 2022-02-16 15:14:58

Ever since [**WebdriverIO**](https://webdriver.io/docs/gettingstarted/) got launched, major companies adopted this tool for automation. It became popular very fast due to its powerful [advantages](https://webdriver.io/). Since the launch, there have been lots of changes and improvements being made to the tool. In this article, we'll be discussing one of the improvements that have really helped us in writing automation scripts in `async` mode.

`WebdriverIO` is asynchronous by nature. Earlier, `WebdriverIO` used to provide the ability to run commands in sync mode using `node-fibers`. However, due to some breaking changes in `Chromium`, `WebdriverIO` discontinued the support for sync mode. Please refer [Sync vs. Async Mode](https://webdriver.io/docs/sync-vs-async) and [this issue](https://github.com/webdriverio/webdriverio/discussions/6702) for more information.

## The test used to look like this:

### With Sync mode:

```js
describe('Sync mode', () => {
  it('does not need await', () => {
    $('#myBtn').click(); // Chaining works
    
    // Chaining works here when using Chain Selector
    $("//div[@class='field']").$("//input[@type='email']").getTagName(); 
  })
})
```

### With Async mode:

```js
describe('Async mode', () => {
  it('needs await', async () => {
    await (await $('#myBtn')).click(); // Needs await keyword twice for chaining
    
    // Similarly in the case below, await keyword is used thrice while using Chain Selector
    await (await (await $("//div[@class='field']").$("//input[@type='email']"))).getTagName();
  })
})
```

As you can see in the above example, for chaining `await` keyword is been used more than once. This can be confusing for someone who is not familiar with the [`async/await`](https://javascript.info/async-await) concept.

## WebdriverIO comes with element chaining support now

Since [v7.9](https://github.com/webdriverio/webdriverio/pull/6954), `WebdriverIO` started supporting element chaining. The same `async` code now can be written as follows:

```js
describe('Async mode', () => {
  it('needs await', async () => {
    await $('#myBtn').click(); 
    
    await $("//div[@class='field']").$("//input[@type='email']").getTagName();
  })
})
```

Now the question comes,  

_Here we are awaiting `$("//div[@class='field']")` which means `$("//div[@class='field']")` returns a promise. So how come we can call .`$("//input[@type='email']")` on the promise returned by `$("//div[@class='field']")`?_

Similar question I faced before while writing test cases. For this, I raised an [issue](https://github.com/webdriverio/webdriverio/issues/7911) on GitHub, and it was answered by WebdriverIO developer team. Let's look into it in more detail below.

## WebdriverIO returns a Promise compatible object

`WebdriverIO` returns a promise compatible object which allows you to do either:

```js
const emailDivField = await $("//div[@class='field']");
const emailFieldTag = await emailDivField.$("//input[@type='email']").getTagName();
```

OR

```js
const emailFieldTag = await $("//div[@class='field']").$("//input[@type='email']").getTagName();
```

_**Promise compatible objects**_ are custom objects which implement the promise interface.

## Caveats

I was upgrading my project with latest version of `WebdriverIO` i.e. `v^7.16.13`. Lessons that I learnt are:

### Chaining won't work for parameters:

If you are passing element as a parameter along with `await` keyword, then in this case chaining won't work.

**Example:**  

Here, we have `Utility` class where we have defined a generic function `isDisplayed()`. This function validates if the list of elements, passed as argument `args`, are visible in the UI.

```js
class Utility {
  async isDisplayed(args) {
    for (const element of args) {
      let isDisplayed = element.isDisplayed();

      if (!isDisplayed) return false;
    }

    return true;
  }
}

export default new Utility();
```

We have `LoginPage` PageObject class. `LoginPage` has 2 elements `pageHeading` and `contactHeading`.

```js
class LoginPage {
  get pageHeading() {
    return $("//h2[text()='Login Page']");
  }
  get contactHeading() {
    return $("//h4[text()='Contact Us']");
  }
}

export default new LoginPage();
```

In the spec file, we are validating if those elements are visible in the UI.

```js
describe('Login screen', () => {
  it('displays all expected headings', async () => {
    const elements = [
      await loginPage.pageHeading,
      await loginPage.contactHeading,
    ];
    let boolVal = await utility.isDisplayed(elements);
    expect(boolVal).to.be.true;
  });
});
```

In the `Utility` class, below line

```js
let isDisplayed = element.isDisplayed(); // Returns Promise
```

won't work as we are calling `isDisplayed()` method in a synchronous manner. But it actually needs `await` keyword. 
```js
let isDisplayed = await element.isDisplayed(); // Works
```

Also passing `await` keyword along with parameters won't work. You can skip using `await` keyword while passing parameters as shown below:

```js
const elements = [
  loginPage.pageHeading,
  loginPage.contactHeading,
];
let boolVal = await utility.isDisplayed(elements);
```

### Use of async/await to handle array of promises

1. **When you want to fetch an array list, then use `Promise.all`**

    ```js
    async getDropdownOptions() {
      const dropdownOptions = await this.dropdownOptions;
      return await Promise.all(
        dropdownOptions.map(function (option) {
          return option.getText();
        }),
      );
    }
    ```

2. **`await Promise.all` won't resolve promise inside function**

    ```js
    async getDropdownOptions() {
      const dropdownOptions = await this.dropdownOptions;
      return await Promise.all(
        dropdownOptions.map(function (option) {
          return option.getText().split('\n')[1]; // Error 
        }),
      );
     }
     ```
 
In above example, you will get an error that says `getText().split()` is not a function. The reason is `getText()` function returns a promise.  You cannot perform a string operation on a promise.

```js
async getDropdownOptions() {
  const dropdownOptions = await this.dropdownOptions;
  return await Promise.all(
    dropdownOptions.map(async function (option) {
      return (await option.getText()).split('\n')[1];
    }),
  );
}
```

## References:

* [\[RFC\] Replacement for Node-Fibers](https://github.com/webdriverio/webdriverio/discussions/6702)
* [Implement new async API](https://github.com/webdriverio/webdriverio/pull/6954)
* [Add support for async iterators](https://github.com/webdriverio/webdriverio/pull/7225)
* [Enable custom chain-able commands with async API](https://github.com/webdriverio/webdriverio/pull/7313)
* [Fix chaining of custom$ and custom$$](https://github.com/webdriverio/webdriverio/pull/7397)
* [WebdriverIO - Sync vs Async mode](https://webdriver.io/docs/sync-vs-async/)