Skip to content

Instantly share code, notes, and snippets.

@ChadKillingsworth
Last active July 6, 2023 06:54
Show Gist options
  • Save ChadKillingsworth/d4cb3d30b9d7fbc3fd0af93c2a133a53 to your computer and use it in GitHub Desktop.
Save ChadKillingsworth/d4cb3d30b9d7fbc3fd0af93c2a133a53 to your computer and use it in GitHub Desktop.
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.

@ChadKillingsworth
Copy link
Author

@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

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

@chiefcll
Copy link

@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

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

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

import shadowDomElement from 'shadow-dom-element'

browser.addCommand("shadowDomElement", shadowDomElement)

@CodeDrivenMitch
Copy link

CodeDrivenMitch 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

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

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

How can I add this to Nightwatch JS?

@Digi27
Copy link

Digi27 commented Jul 6, 2023

@chiefcll Did you proceed further for nightwatch.js?

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