Skip to content

Instantly share code, notes, and snippets.

@dhwang
Forked from ChadKillingsworth/e2e-shadowdom.md
Created April 8, 2021 07:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dhwang/dca5feefb84acfeb918892a2ff61508d to your computer and use it in GitHub Desktop.
Save dhwang/dca5feefb84acfeb918892a2ff61508d 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.

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