Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Selenium Testing with Shadow DOM

End-to-end Testing with Shadow DOM

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

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:

Example Shadow DOM Tree

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:

Example Shady DOM Tree

In this case, the same command works - however now the selectors are joined and the element is returned by a single query.

Closed Shadow Roots

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.

@TakayoshiKochi
Copy link

TakayoshiKochi commented Dec 28, 2016

FYI, >>> combinator, which is same as /deep/, but only works for v1 open shadow roots, can be used in querySelector() context
(i.e. not in style sheets) with the experimental flag in Chrome 56+ (Chromium#633007).

There is still contention about the spec for this, so if you have any feedback on this use case, please chime in at WICG/webcomponents#78.

@kartikeya27
Copy link

kartikeya27 commented Jan 13, 2017

Where I can write custom webdriver.io command ? Inside wdio.conf.js It would nice, if you could give more detail step by step example. I'm just looking for a tool which can do functional testing for shadow-dom kind of web-site in chrome. WCT works fine with unit testing but not with functional testing. Any idea or suggestion would be greatly appreciated.

@ChadKillingsworth
Copy link
Author

ChadKillingsworth commented Jan 21, 2017

@TakayoshiKochi thanks for the update. I'd watched the discussion on the shadow piercing combinator but hadn't realized Chrome had decided to go ahead with it.

However, until other shadow dom enabled browsers follow suit, it will be of little help.

@ChadKillingsworth
Copy link
Author

ChadKillingsworth commented Jan 21, 2017

@kebijebi Webdriver.io has great documentation: http://webdriver.io/guide/usage/customcommands.html

@chiefcll
Copy link

chiefcll commented Jul 18, 2017

@ChadKillsworth - I'm trying to get this snippit to work - however, the execute command can not return a dom element. You get something like:

{ state: 'success',
  sessionId: '7c47a100-a455-428f-ae97-4ee6b146e10b',
  hCode: 1113251469,
  value: { ELEMENT: '4' },
  class: 'org.openqa.selenium.remote.Response',
  status: 0 }

You can get the element in the JS layer, but it will get back through selenium, meaning no click or any other interaction. Also it looks like /deep/ and >>> are both deprecated. I think we're out of luck with ShadowDom. I'm going to try to get the element coordinates and then use that to focus it and return the activeElement.

@chiefcll
Copy link

chiefcll commented Jul 18, 2017

Update: I was able to get the snippit to work with nightwatchjs. You can return a dom element! The below snippit works...

client.shadowDomElement([['my-app', 'my-component', 'a.button']], elm => {
      client.elementIdClick(elm.value.ELEMENT);
    });

I'll work on making more progress. First note is wrapping the first arg in additional array, I assume nightwatch uses .apply to pass the args along.

@elf-pavlik
Copy link

elf-pavlik commented Sep 26, 2017

@ChadKillingsworth have you considered releasing it on npm as a module?

import shadowDomElement from 'shadow-dom-element'

browser.addCommand("shadowDomElement", shadowDomElement)

@Morlack
Copy link

Morlack commented Nov 2, 2017

I was inspired by your solution and decided to make it a webdriverio plugin!
The plugin overrides the element and elements commands, which all other internal commands use. Most of your code will work as normal, but you might need to adjust some selectors.

https://www.npmjs.com/package/wdio-webcomponents

@ChadKillingsworth
Copy link
Author

ChadKillingsworth commented Dec 28, 2017

I am just now seeing recent comments. Github doesn't notify you when someone comments on a gist.

@BillalPatel
Copy link

BillalPatel commented Jan 16, 2018

Hi All,

Can somebody please tell me how I would use this approach to get an array of elements as opposed to a single element?

Thanks!

@batraanupriya
Copy link

batraanupriya commented May 14, 2019

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.

@AllanOricil
Copy link

AllanOricil commented Aug 4, 2020

How can I add this to Nightwatch JS?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment