WebdriverIO supports Chaining without multiple await statements
Mayank Shukla
Posted on February 16, 2022
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
-
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(); }), ); }
-
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:
Posted on February 16, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.