WebdriverIO v9 Released
The whole Webdriverio development team is stoked and proud to release WebdriverIO v9 today!
This marks the beginning of a new exciting era for all projects using WebdriverIO as their test automation tool. With the great work of browser teams that are working on e.g. Chrome and Firefox, we now enter a new era that offers much greater automation capabilities than ever before thanks to the new WebDriver Bidi protocol. With WebdriverIO v9 we have been working to be at the forefront of adopters of this new era and allow you to leverage from the power of the protocol first.
Let's explore the new features, updates, and optimizations in this release.
New Features
The majority of the new features in v9 are enabled by the WebDriver Bidi capabilities now available in browsers. Upon upgrading to v9, all sessions will automatically use Bidi unless you explicitly disable it via the new wdio:enforceWebDriverClassic
capability.
These features unfortunately won't be available to you if your remote environment doesn't support WebDriver Bidi. You can check if Bidi is support in your session by looking into the browser.isBidi
property.
New url
Command Parameters
The url
command has evolved from a simple navigation tool to a powerful feature-packed command.
Passing in Custom Headers
You can now pass custom headers that will be applied when the browser makes a request. This is useful for setting session cookies to log in automatically.
await browser.url('https://webdriver.io', {
headers: {
Authorization: 'Bearer XXXXX'
}
});
Note that while you can modify how the browser treats all requests through the mock
command, changes applied to the url
command only last for that particular page load and reset once navigation is complete.
Overcome Basic Authentification
Automating user authentication via basic authentication has never been easier:
await browser.url('https://the-internet.herokuapp.com/basic_auth', {
auth: {
user: 'admin',
pass: 'admin'
}
});
await expect($('p=Congratulations! You must have the proper credentials.').toBeDisplayed();
Running an Initialization Script
Inject JavaScript before anything loads on the website using the beforeLoad
parameter. This is useful for manipulating Web APIs, e.g.:
// navigate to a URL and mock the battery API
await browser.url('https://pazguille.github.io/demo-battery-api/', {
onBeforeLoad (win) {
// mock "navigator.battery" property
// returning mock charge object
win.navigator.getBattery = () => Promise.resolve({
level: 0.5,
charging: false,
chargingTime: Infinity,
dischargingTime: 3600, // seconds
})
}
})
// now we can assert actual text - we are charged at 50%
await expect($('.battery-percentage')).toHaveText('50%')
// and has enough juice for 1 hour
await expect($('.battery-remaining')).toHaveText('01:00)
In this example, we overwrite the getBattery
method of the Navigator
interface.
New addInitScript
Command
The addInitScript
command enables you to inject a script into the browser that is triggered every time a new browsing context is opened. This includes actions such as navigating to a URL or loading an iframe within an application. The script you pass to this command receives a callback as its last parameter, allowing you to send values from the browser back to your Node.js environment.
For instance, to get notified whenever an element is added or removed from a node in the application, you can use the following example:
const script = await browser.addInitScript((myParam, emit) => {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
emit(mutation.target.nodeName)
}
})
observer.observe(document, { childList: true, subtree: true })
})
script.on('data', (data) => {
console.log(data) // prints: BODY, DIV, P, ...
})
This initialization script can modify global variables and overwrite built-in Web API primitives, allowing you to configure the test environment to meet your specific requirements.
Cross Browser Request Mocking
WebdriverIO introduced request mocking in v6.3.0
, limited to Chromium browsers. With v9, we now use WebDriver Bidi, extending support to all browsers. This enhancement allows modifying requests before they go out to the network:
// mock all API requests
const mock = await browser.mock('**/api/**')
// apply auth token to each request
mock.request({
headers: { 'Authorization': 'Bearer XXXXXX' }
})
Automatic Shadow Root Piercing
Testing applications with Web Components is now seamless with automatic shadow root piercing. WebdriverIO now keeps track of all ShadowRoot
nodes and searches across them.
For example, let's say we want to automate the following Date Picker component that contains multiple nested shadow roots, take a look with Right Click > Inspect
on any of the elements in the application and observe how much they are nested within different shadow roots:
In order to change the date, you can now just call:
await browser.url('https://ionicframework.com/docs/usage/v8/datetime/basic/demo.html?ionic:mode=md')
await browser.$('aria/Sunday, August 4]').click()
await browser.$('.aux-input').getValue() // outputs "2024-08-04T09:00:00"
While WebdriverIO previously supported "deep selectors", this functionality was limited to CSS selectors. The new selector engine now supports all selector types, including accessibility labels.
With the enhanced locator engine, elements within multiple nested shadow roots can be easily found. WebdriverIO is the first framework to support this capability for shadow roots in both open
and closed
mode.
Improved Argument Serialization
With WebDriver Classic the ability to move data objects from the Test to the browser environment was rather limited to DOM elements and serializable objects and types. With WebDriver Bidi we are now able to do a better job transforming non-serializable data objects to be used in the browser as the object that it is. Alongside known JavaScript primitives such as Map
or Set
, the protocol allows to serialize values such as Infinity
, null
, undefined
and BigInt
.
Here is an example where we compose a JavaScript Map object in Node.js and pass it over to the browser where it gets automatically deserialzed back into a Map and vise versa:
const data = new Map([
['username', 'Tony'],
['password', 'secret']
])
const output = await browser.execute(
(data) => `${data.size} entrie(s), username: ${data.get('username')}, password: ${data.get('secret')}`,
data
)
console.log(output)
// outputs: "1 entrie(s), username: Tony, password: secret"
This will make it easier pass data along and work with custom scripts that can now return rich data objects that will help better observe the state of your application. It will allow frameworks like WebdriverIO to integrate deeper with the browser environment and build more useful features in the future.
Setting Viewports
While WebdriverIO allows you to test your application in both desktop and mobile browsers, it is often easier to emulate a mobile user by adjusting the browser viewport to check if the application renders properly in responsive mode. With WebDriver Classic, this can be challenging because browser windows cannot scale down to very small sizes and often maintain a minimum width of 500px
. For example:
await browser.url('https://webdriver.io')
await browser.setWindowSize(393, 659)
console.log(await browser.execute(() => window.innerWidth)) // returns `500`
WebdriverIO v9 introduces the setViewport
command, enabling you to adjust the application’s viewport to any size, including modifying the devicePixelRatio
. Unlike setWindowSize
, which changes the overall browser window size, setViewport
specifically resizes the canvas where the application is rendered, leaving the browser window dimensions unchanged.
To simplify mobile device emulation, we’ve enhanced the emulate command. This new capability allows you to simultaneously adjust the viewport size, device pixel ratio, and user agent for a specific mobile device by simply specifying its name. For example:
await browser.url('https://webdriver.io')
await browser.emulate('device', 'iPhone 15')
console.log(await browser.execute(() => window.innerWidth)) // returns `393`
console.log(await browser.execute(() => navigator.userAgent)) // returns `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36`
While we recommend to run mobile testing on actual mobile devices, as mobile browser engines differ from the ones used for desktop browser, this can be an easy escape hatch if we just quickly want to verify how the application renders in mobile viewports.
Fake Timers Support
Want to change the time in the browser? With WebdriverIO v9, it's now possible to fake the time within the browser for your tests. We've enhanced the emulate
command with a new property: clock
. This allows you to set the date and time to whatever you need and control when time should advance. Here's how it works:
const clock = await browser.emulate('clock', { now: new Date(2021, 3, 14) })
console.log(await browser.execute(() => new Date().getTime())) // returns 1618383600000
await clock.tick(1000)
console.log(await browser.execute(() => new Date().getTime())) // returns 1618383601000