Today's web applications are not pieced together using only the standard HTML elements of yesteryear. We now have the ability to use Web Components as a way to encapsulate styling and functionality for particular components, which means we can easily share them and reuse them within and across apps.
Check out: Reliably Opening Deep Links Across Platforms and Devices
This is great, but when it comes to testing this new breed of web app, things get more complicated. The way Web Components are implemented, the "interior" of a component does not appear in the DOM; instead, it appears in a separate tree called a "Shadow DOM", which is linked into the "main" DOM at the point we want it to appear. This separation is a good thing, because we don't want unrelated style definitions for our main document to be able to target the interior of a 3rd-party component. But what if we want to make use of an element located in the interior of a Web Component? We can't just access it as if it were part of the DOM, because it's hidden away inside its own special Shadow DOM.
Also check: A Complete Guide to Web App Testing
The shadowRoot of an element is itself an element, namely the root element inside the component. Using the executeScript method of our Appium driver, we can actually retrieve this shadow root as a standard WebElement:
What's going on here? Basically, we're constructing a little snippet of JS which will be run in the context of the currently loaded page. But we're not running it free of any context---we're actually passing in an element we've found as an argument to the script. Appium knows how to convert your client-side WebElement into an actual HTML element in the context of the executing JS, which means we can access the standard properties available on HTML elements, including shadowRoot. Finally, we're simply returning the shadow root and casting it to a WebElement so we can work with it in our test script.
Read: How to Write Automated Test Scripts Using Selenium
Now, if we were dealing with Selenium, we would be able to use this shadowRoot element just like any other element (for example in order to find an input field inside the component):
However, due to bugs and/or limitations in the technologies that Appium relies on (Chromedriver for Android, and the Selenium Atoms / remote debug protocol for Safari), this will not work. We'll get different errors on the two platforms, but either way we're stuck. What to do?
Also read: A complete guide to Selenium testing
With this little trick, we're able to now use innerElement just as if it were a regular old main DOM element, for example with commands like innerElement.isSelected() and so on. And that's how we can work with elements inside the shadow DOM! How did we know we were dealing with a shadow DOM to begin with? It's easy to tell by using the dev tools inspector in a browser like Firefox.
See: Chrome - DevTools: A Complete Guide
Closed Shadow DOMs
I've omitted one wrinkle, which is that shadow DOMs come in two flavors: open and closed. Closed shadow DOMs are unfortunately not accessible via the shadowRoot property, which means the strategy above will not work in accessing them. Luckily, closed shadow DOMs are considered unnecessary and are not too common. There are ways of dealing with them, nonetheless, which we will cover in another edition of Appium Pro.
Due to the differences in how Appium supports testing website on Android vs iOS, there is another difference you have to consider in writing your tests, which is that for Safari, using any element found within a shadow DOM will always generate a StaleElementException when you try and use it. This means the strategy above (where we get a WebElement out of a shadow DOM using executeScript), is not going to work.
Also see: Designing an Automated Functional Testing Strategy
Instead, for Safari, we have to stay completely within JS-land if we want to successfully automate the shadow DOM. For example, let's say our goal was to check the state of a checkbox input inside of a custom component. With Chrome, we are able to simply find the checkbox using the strategy above, and call isSelected() on it to determine whether it's checked. With Safari, calling isSelected() will result in a StaleElementException. So what we must do instead is something like the following:
This is a completely reliable automation technique; however, it relies purely on JS and doesn't stay within the typical WebDriver API usage, which is not ideal.
It's worth pointing out that if you have to automate both Chrome and Safari, the pure-JS tactic I just described will work for both; so, in cross-platform situations it's probably better to use this latter technique rather than mixing two different strategies together.
Have a look at a full sample which pierces the shadow DOM of a Material Web Component to determine the input state of a switch. (Of course, the designers of this component were smart and made that checked state bubble up to a property on the component itself, so piercing the shadow DOM is not strictly necessary. It is however useful for instruction purposes here).
Notice how both strategies are represented, and that the Android test runs both strategies in the same test, to prove that they both work for Android. Don't forget to also have a look at the sample on GitHub.