Ever since WebdriverIO got launched, major companies adopted this tool for automation. It became popular very fast due to its powerful advantages. 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 and this issue for more information.

The test used to look like this:

With Sync mode:

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:

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 concept.

WebdriverIO comes with element chaining support now

Since v7.9, WebdriverIO started supporting element chaining. The same async code now can be written as follows:

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 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:

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

OR

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.

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.

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.

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

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.

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:

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

    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

    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.

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

References: