Forem

Cover image for WebdriverIO supports Chaining without multiple await statements
Mayank Shukla
Mayank Shukla

Posted on • Originally published at blog.kiprosh.com

1 1

WebdriverIO supports Chaining without multiple await statements

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(); 
  })
})
Enter fullscreen mode Exit fullscreen mode

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();
  })
})
Enter fullscreen mode Exit fullscreen mode

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();
  })
})
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

OR

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

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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;
  });
});
Enter fullscreen mode Exit fullscreen mode

In the Utility class, below line

let isDisplayed = element.isDisplayed(); // Returns Promise
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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];
    }),
  );
}
Enter fullscreen mode Exit fullscreen mode

References:

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay