As the web component specs continue to be developed, there has been little information on how to test them.
In particular the /deep/
combinator has been deprecated in Shadow DOM 1.0. This is particularly painful since
most end-to-end testing frameworks rely on elements being discoverable by XPath or calls to querySelector.
Elements in Shadow DOM are selectable by neither.
Webdriver.io has the standard actions by selectors, but also allows browser executable scripts to return an element which can then be acted upon. Here's a custom webdriver.io command to return an element in a Shadow DOM tree:
/**
* This function runs in the browser context
* @param {string|Array<string>} selectors
* @return {?Element}
*/
function findInShadowDom(selectors) {
if (!Array.isArray(selectors)) {
selectors = [selectors];
}
function findElement(selectors) {
var currentElement = document;
for (var i = 0; i < selectors.length; i++) {
if (i > 0) {
currentElement = currentElement.shadowRoot;
}
if (currentElement) {
currentElement = currentElement.querySelector(selectors[i]);
}
if (!currentElement) {
break;
}
}
return currentElement;
}
if (!(document.body.createShadowRoot || document.body.attachShadow)) {
selectors = [selectors.join(' ')];
}
return findElement(selectors);
}
/**
* Add a command to return an element within a shadow dom.
* The command takes an array of selectors. Each subsequent
* array member is within the preceding element's shadow dom.
*
* Example:
*
* const elem = browser.shadowDomElement(['foo-bar', 'bar-baz', 'baz-foo']);
*
* Browsers which do not have native ShadowDOM support assume each selector is a direct
* descendant of the parent.
*/
browser.addCommand("shadowDomElement", function(selector) {
return this.execute(findInShadowDom, selector);
});
/**
* Provides the equivalent functionality as the above shadowDomElement command, but
* adds a timeout. Will wait until the selectors match an element or the timeout
* expires.
*
* Example:
*
* const elem = browser.waitForShadowDomElement(['foo-bar', 'bar-baz', 'baz-foo'], 2000);
*/
browser.addCommand("waitForShadowDomElement", function async(selector, timeout, timeoutMsg, interval) {
return this.waitUntil(() => {
const elem = this.execute(findInShadowDom, selector);
return elem && elem.value;
}, timeout, timeoutMsg, interval)
.then(() => this.execute(findInShadowDom, selector));
});
For examples, let's use the following Shadow DOM:
The above webdriver.io custom command can then be used like so:
browser.shadowDomElement(['foo-bar', 'bar-baz', 'button'])
.click();
But what about the case when using the Shady DOM polyfill? The same script can work. Here's the same dom tree using Shady DOM rather than native Shadow DOM:
In this case, the same command works - however now the selectors are joined and the element is returned by a single query.
With the Shadow DOM v1 spec, shadow roots can be closed. For closed roots, walking down the tree using the shadowRoot
property just isn't going to work. It should be possible to save a reference to the shadow tree and expose that for testing,
but I've not verified that yet.
This works perfectly with chrome but with firefox it is not able to find the parameter.
this.browser.shadowDomElement(['a','b','v']).$('#id').getText() -> Cannot find element with #id in firefox.