ReactJS is one of the most widely use Front-End libraries in the web. Along side React, many developers use styling tools that will minify or re-write the class attribute values attached to the HTML elements via className props in JSX. These minifications and overwrites make it difficult to select the generated HTML using the WebDriver's query commands like findElement or findElements since it's not guaranteed that the class name will remain the same.
Today we introduce two new commands, browser.react$ and browser.react$$, to WebdriverIO's browser object that allows you to query for a single or multiple React component instances in the page with an easy to use API. These new commands will return the WebdriverIO element(s) for the query in where you will have access to the complete element commands API.
Internally, WebdriverIO uses a library called resq to query React's VirtualDOM in order to retrieve the nodes. This library allows WebdriverIO to find any component in the VirtualDOM by the component's name and also filter this selection by state and/or props.
WebdriverIO's provided API, browser.react$ and browser.react$$, methods have three parameters. The first parameter is the selector to query, this parameter is required. The second and third parameters are optional filters, props and state respectively.
const selector ='MyComponent' const propFilter ={ someProp:true} const stateFilter ='this is my state' browser.react$(selector,{ props: propFilter, state: stateFilter })
In the examples we will cover basic usages for all three parameters.
Now, let's say we want to test that the first instance of MyComponent is correctly displayed in the browser. Well, with the browser.react$ command, we can select this first instance and then query against it.
// spec/mycomponent.test.js test('it should be displayed',()=>{ const myComponent = browser.react$('MyComponent') expect(myComponent.isDisplayed()).toBe(true)// pass })
Simple, no? But what if we want to select the component that says Hello WebdriverIO and verify that the text is correct? Well, we can filter our queries!
In React, the props will always be an object so for this filter parameter we can only pass an object to be used to filter our results.
You might've noticed that in our component we have a state that adds extra text if the name matches there. We can select this component by filtering the components by their current state.
// spec/mycomponent.test.js test('it should correctly display "Hello WebdriverIO"',()=>{ const myComponent = browser.react$('MyComponent',{ state:', how are you?' }) expect(myComponent.getText()).toBe('Hello there, how are you?')// pass })
As you can see, for the state filter we pass the string that equals to the current state of the component, this last parameter in the function can be any of the following: string, number, boolean, array, or object. This is because all these types are valid state types for React.
By now you might be wondering why we are using browser.react$ in all the examples. Well, both commands have the same parameters and work almost the same with the only difference being that browser.react$$ will return an array of all the WebdriverIO elements corresponding to the selector and/or filter match.
We are very pleased with this addition and we hope you can take full advantage of it. We suggest you use React Dev Tools, using this tool will help you see how the components in the application are called, which props they have, and which state they are currently in. Once you know this information, using WebdriverIO's React API will be a lot easier.
Note: This blog post was updated after the v6 release to reflect changes to the command interface.
Shadow DOM is one of the key browser features that make up web components. Web components are a really great way to build reusable elements, and are able to scale all the way up to complete web applications. Style encapsulation, the feature that gives shadow DOM it's power, has been a bit of a pain when it comes to E2E or UI testing. Things just got a little easier though, as WebdriverIO v5.5.0 introduced built-in support for shadow DOM via two new commands, shadow$ and shadow$$. Let's dig into what they're all about.
With v0 of the shadow DOM spec, came the /deep/ selector. This special selector made it possible to query inside an element's shadowRoot. Here we're querying for a button that is inside the my-element custom element's shadowRoot:
With /deep/ being deprecated and subsequently removed, developers found other ways to get at their shadow elements. The typical approach was to use custom commands in WebdriverIO. These commands used the execute command to string together querySelector and shadowRoot.querySelector calls in order to find elements. This generally worked such that, instead of a basic string query, queries were put into arrays. Each string in the array represented a shadow boundary. Using these commands looked something like this:
The downside of both the /deep/ selector and the javascript approach was that in order to find an element, the query always needed to start at the document level. This made tests a little unwieldy and hard to maintain. Code like this was not uncommon:
These commands take advantage of the $ command in WebdriverIO v5's ability to use a function selector. They work just like the existing $ and $$ commands in that you call it on an element, but instead of querying an element's light DOM, they query an element's shadow DOM (they fall back to querying light dom if for whatever reason, you're not using any polyfills).
Since they're element commands, it's no longer required to start at the root document when building your queries. Once you have an element, calling element.shadow$('selector') queries inside that element's shadowRoot for the element that matches the given selector. From any element, you can chain $ and shadow$ commands as deeply as needed.
Like their counterparts, $ and $$, the shadow commands make page objects a breeze to write, read and maintain. Let's assume we're working with a page that looks something like this:
This uses two custom elements, my-app and app-login. We can see that my-app is in the body's light DOM, and inside it's light DOM is an app-login element. An example of a page object to interact with this page might look like so:
classLoginPage{ open(){ browser.url('/login'); } getapp(){ // my-app lives in the document's light DOM return browser.$('my-app'); } getlogin(){ // app-login lives in my-app's light DOM returnthis.app.$('app-login'); } getusernameInput(){ // the username input is inside app-login's shadow DOM returnthis.login.shadow$('input #username'); } getpasswordInput(){ // the password input is inside app-login's shadow DOM returnthis.login.shadow$('input[type=password]'); } getsubmitButton(){ // the submit button is inside app-login's shadow DOM returnthis.login.shadow$('button[type=submit]'); } login(username, password){ this.login.setValue(username); this.username.setValue(password); this.submitButton.click(); } }
In the example above, you can see how it's easy to leverage the getter methods of your page object to drill further and further into different parts of your application. This keeps your selectors nice and focused. For example, should you decide to move the app-login element around, you only have to change one selector.
Following the page object pattern is really powerful on its own. The big draw of web components is that you can create reusable elements. The downside with only using page objects though, is that you might end up repeating code and selectors in different page objects to be able to interact with the elements encapsulated in your web components.
The component object pattern attempts to reduce that repetition and move the component's api into an object of its own. We know that in order to interact with an element's shadow DOM, we first need the host element. Using a base class for your component objects makes this pretty straightforward. Here's a bare-bones component base class that takes the host element in its constructor and unrolls that element's queries up to the browser object, so it can be reused in many page objects (or other component objects), without having to know anything about the page itself:
classComponent{ constructor(host){ const selectors =[]; // Crawl back to the browser object, and cache all selectors while(host.elementId&& host.parent){ selectors.push(host.selector); host = host.parent; } selectors.reverse(); this.selectors_= selectors; } gethost(){ // Beginning with the browser object, reselect each element returnthis.selectors_.reduce((element, selector)=> element.$(selector), browser); } } module.exports=Component;
We can then write a subclass for our app-login component:
Finally, we can use the component object inside our login page object:
constLogin=require('./components/login'); classLoginPage{ open(){ browser.url('/login'); } getapp(){ return browser.$('my-app'); } getloginComponent(){ // return a new instance of our login component object returnnewLogin(this.app.$('app-login')); } }
This component object can now be used in tests for any page or section of your app that uses an app-login web component, without having to know about how that component is structured. If you later decide to change the internal structure of the web component, you only need to update the component object.
Currently the WebDriver protocol does not provide native support for shadow DOM, but there has been progress made for it. Once the spec is finalized, WebdriverIO will implement the spec. There's a decent chance that the shadow commands will change under the hood, but I'm pretty confident that they're usage will be the same as it is today, and that test code that uses them will need little to no refactoring.
IE11-Edge: Shadow DOM is not supported in IE or Edge, but can be polyfilled. The shadow commands work great with the polyfills.
Firefox: Calling setValue(value) on an input field in Firefox results in an error, complaining that the input is "not reachable by keyboard". A workaround for now is to use a custom command (or method on your component object) that sets the input field's value via browser.execute(function).
Safari: WebdriverIO has some safety mechanisms to help mitigate issues with stale element references. This is a really nice feature but unfortunately Safari's webdriver does not provide the proper error response when attempting to interact with what in other browsers, is a stale element reference. This is unfortunate but at the same time, it's generally a bad practice to cache element references. Stale element references are typically completely mitigated by using the page and component object patterns outlined above.
We are pleased to announce that a new major version of WebdriverIO has finally been released! I never thought that it would take this long (it's been over a year), but we can finally say that the new version of WebdriverIO is ready for use (and better than ever). There has been over 800 commits, from over 34 different contributors; and I am truly grateful to everyone who participated in this collective effort. With that being said...
When I began to practically rewrite this project from scratch one year ago, I knew there would be problems here and there. However, I was confident that the WebdriverIO community in the support channels would work collectively to help each other out; and I'm proud to say, I was right!
Looking into the history of the project, it is humbling to see how much it has been grown within just a few years. From a handful of downloads a day, to now almost 50k; as well as the knowledge that big companies are relying on this tool to ship their software on daily basis, it became obvious that the next big feature had to be sustainability. The first step was made by joining the JS.Foundation at of 2017.
The next step was to implement a technical infrastructure that would allow the project to grow. By reviewing other successful open source projects such as e.g. Jest or Babel, we adapted a monolithic project structure to simplify the process of contributing to WebdriverIO.
We wanted to start this effort completely community driven, and began to gather [feedback](https://github.com/webdriverio/webdriverio/issues/2403) from everyone who was using WebdriverIO on daily basis. We created a [Gitter channel](https://gitter.im/webdriverio/webdriverio) in order to discuss architectural changes, and to organize the work of porting the packages into the new tech stack that was [Lerna](https://lernajs.io/).
As we moved to a monolithic system we scoped all WebdriverIO packages into the @wdio NPM organization. This would make it simpler to onboard contributors to release new package versions, and better clarifies which packages are "officially" maintained by the organization or are 3rd party community packages.
If you have been using `wdio-mocha-framework` or `wdio-spec-reporter` in your project please update the packages to use the ones built for v5: `@wdio/mocha-framework` or `@wdio/spec-reporter`. Going forward the version number of all packages are now pinned to each other; meaning that you should always have the same versions for all "official" WebdriverIO packages that you use.
When people proposed new commands to the API, we contributors became more and more hesitant to introduce them. There were a vast number of existing commands; and many of these requested commands provided very little difference from existing commands. This was beginning to become a maintenance nightmare.
Starting with v5 we created a "base" WebdriverIO package called [`webdriver`](https://www.npmjs.com/package/webdriver). It contains the bare logic to make a HTTP request to a WebDriver endpoint; and includes all of the commands from [various specifications](https://github.com/webdriverio/webdriverio/tree/main/packages/webdriver/protocol) (including the [WebDriver](https://w3c.github.io/webdriver/) spec as well as Appium's [Mobile JSONWire protocol](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md)) defined in a simple to maintain JSON object.
As part of that effort we've renamed many of the commands in order to align them closer to the pattern that has been used in the protocol. The base WebDriver client now also returns the `value` property of all protocol command responses, so that we were able to get rid of a lot of redundancy (e.g. `title` is now called with `getTitle`).
Additionally, we have seen significant confusion regarding the WebdriverIO API, and the fact that it sometimes took selectors as a first argument, and sometimes not. This was causing various problems e.g. building a consistent and useful TypeScript definition for it. In the new version __we got rid of selectors as command parameters__, and enforced the difference between commands only accessible from the browser/client instance, and those from the element instance. In order to click on an element, you must now first fetch it, and then call the click command on that instance, e.g.
browser.click('#elem')// throws 'browser.click is not a function' const elem =$('#elem') elem.click()// clicks successfully on the element // or $('#elem').click() elem.url('https://webdriver.io')// throws because the url command is scoped on the browser object
We already had an automated system that generated the docs for our website. As part of the new architecture and tech stack we however ported this from Hexo to Docusaurus.
We are still in the process of finalizing this effort, as we want to continue providing everyone a way to easily [change the version](https://github.com/webdriverio/webdriverio/issues/3147) of the docs. We are also looking into [providing multiple translations](https://github.com/webdriverio/webdriverio/issues/3148) of the docs so that people who don't speak english can better understand and use WebdriverIO. Please reach out to us on [Twitter](https://twitter.com/webdriverio), or directly on the issue thread if you want to help out. This is probably one of the best ways to get involved into an open source project.
There are significantly more things that we have been working on over the last year that you might like to read about. Check out the official changelog to find all of the changes that describe the new version. We will probably continue to update this over time, as we weren't able to keep a list of every detail that changed. We would also like to ask you to have a look into the new guide section and our updated docs in general.
There is unfortunately no easy upgrade tool that you can download and run to update your test suites from v4 to v5 (even though we would love to have such a thing, PRs are welcome ๐). If you run into any issues upgrading to v5 please join our support [](https://gitter.im/webdriverio/webdriverio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) and reach out to us.
Every project is different, so it is impossible to have one single guide for everyone. However, the following step by step description will help you get closer to where you need to be:
- read the [changelog](https://github.com/webdriverio/webdriverio/blob/main/CHANGELOG.md#v500-2018-12-20) to understand all breaking changes - remove all `wdio-*` packages from your `package.json` - remove your `node_modules` directory - install the latest version of webdriverio: `$ npm install webdriverio@latest` - install the new wdio testrunner: `$ npm install @wdio/cli --save-dev` - if you have a `wdio.conf.js` in your root directory, create a backup: `$ cp wdio.conf.js wdio_backup.conf.js` - rerun the configuration wizard: `$ npx wdio config` - merge custom modifications of your old `wdio_backup.conf.js` into your new config file. Don't merge everything at once - just begin with the basic setup using no services and just the e.g. spec reporter to run tests locally and work towards a full migration - take the simplest test in your suite and rename the commands according to the changelog - have your log directory set in your config (e.g. `outputDir: __dirname`) to ensure you can see everything that is going on including errors (you can later set it to a proper log directory) - attempt to run the the test suite you modified `$ npx wdio wdio.conf.js --spec ./path/to/modified/test.js` - repeat on your remaining test files - add reporters and services back into your `wdio.conf.js`, and see if they work as expected (__Note:__ it is possible that services or reporters that you have used aren't ported to v5 yet, if so, please raise an issue in the repository of that community package or try to port it) If you have issues porting your test suite, check the issues thread to see if someone has already reported the same problem; and then reach out to us on our gitter channel. We may have missed porting / not yet ported a functionality that you have been using in your test. Thanks to the new project structure, we can quickly fix this and provide an update version for you!
We will release further blog articles in our new blog with tutorials on how to upgrade WebdriverIO to v5 soon. You can also checkout the excellent [video series](https://www.youtube.com/watch?v=MO8xeC-w2Og&list=PL8HowI-L-3_9Ep7lxVrRDF-az5ku4sur_) from our beloved [Will Brock](https://twitter.com/willbrock) on the new release. An update to the [WebdriverIO Learning Course](https://learn.webdriver.io/) is also already in work.
We didn't put put all this hard work into the project to end here. Instead, we just get started. We truly believe that WebDriver is and always will be the automation standard of the industry. Therefore we are actively engaged in contributing to the standard to ensure that all commands are compliant to the protocols. We also will take your common problems and general feedback back to the W3C Working Groups in order to ensure that we can address major issues at the core of the technology.
To ensure that @webdriverio is always conforming to the #WebDriver standard, it will sends its representatives directly to the @w3c TPAC meetings so you can use the latest features as soon as they are available ๐๐ป pic.twitter.com/oJbHPn99Oc
After the release, we will be begin working on a detailed roadmap for the next year that allows you to participate in the progress and help us prioritizing features.
Before I close up this blog post, I want to address that WebdriverIO is an open source project maintained by dedicated people who love open source. Often, I get the feeling that people take this work for granted and forget that there are real humans spending their free time to provide some piece of software to everyone for free. As @left_pad stated in the official Babel7 release:
"[...] I can only describe it as dread when seeing a message from someone online wondering why something hasn't been released while another asks why this bug isn't fixed yet. I want to just rush it out and be done with it but I also have a desire to take this seriously."
Please always be reminded that when you open an issue or ask for a feature that you are basically asking someone to spend his time on something to give you exactly that for free! Open source project can only work and survive if everyone participates and helps out with something. We are all humans and bugs happen, if you find them, please let us know in a way that we can easily work with the information and quickly fix it. It's even better if everyone once in a while takes some time of his day to contribute back. We are maintaining a list of well described issues that anyone can claim and start working on.
Again, this project is not sponsored nor owned by any company. It is a collaborative effort of an inclusive community that loves to help each other out. Let's continue to stay exactly that!
Finally, I want to thank a few people that have helped to make this release happen; and/or for being such great people in the community. First, I want to thank all 34 contributors that participated in making this happen, as well as all the other 328 people that have been collaborating since the beginning.
I personally want to thank [Kevin Lamping](https://twitter.com/klamping) for making a stellar [online learning course](https://learn.webdriver.io/) as well as so so much other great content on [YouTube](https://t.co/WX6flUsZ8e) over the last few years. As mentioned above - also a big thank you to [Will Brock](https://twitter.com/willbrock) for his [video course](https://www.youtube.com/watch?v=MO8xeC-w2Og&list=PL8HowI-L-3_9Ep7lxVrRDF-az5ku4sur_) on the new release.
I want to thank everyone who has been deeply involved in the support channel helping people out on daily basis. With over 3500 users it became impossible for me to answer everyone on the level I did before. Thank you Erwin Heitzman, Tu Huynh, Jim Davis, Xu Cao, Boris Osipov and Wim Selles.
Big shoutout to Daniel Chivescu and Josh Cypher who have been giving talks on WebdriverIO around the world.
And last but not least thank you [Adam Bjerstedt](https://github.com/abjerstedt), who has not only been helping out in the community channel; but also for helping to push out the v5 release at the end.