End-to-end Testing
How-tos & Guides
8 min read

Testing Shadow DOM elements in Selenium

Learn about existing workarounds and new built-in behavior in Selenium that lets you interact with Shadow DOM elements in your automated tests.

David Adeneye
Published August 26, 2022

Introduction

Shadow DOM is a relatively new addition to the W3C spec which enables web developers to create isolated and reusable components. While there are major benefits to using Shadow DOM, one of the historic drawbacks has been poor testing support.

In this article, we’ll cover how to access and manipulate Shadow DOM elements within Selenium tests, both via the new native support for Shadow DOM in Selenium 4, as well as using workarounds that are compatible with older versions of Selenium.

Prerequisites

Here are the prerequisites you’ll need to follow along with this tutorial:

What is Shadow DOM

If you’re a web developer, then you’re already familiar with the concept of the Document Object Model, or DOM for short. The DOM represents the underlying structure of a web page and is queryable via JavaScript APIs like getElementById and querySelectorAll.

Shadow DOM, which was first implemented in Chrome in 2014 and is now supported across all modern browsers, provides a mechanism that allows hidden DOM trees to be attached to individual elements within a page. Just like with the Document Object Model, a new set of JavaScript APIs enables developers to manipulate Shadow DOM trees. Each Shadow DOM element contains a set of nodes that make up a tree structure and which, by definition, have a single common ancestor. Whereas the common ancestor in the parent web page is the <html> element, in Shadow DOM the root ancestor is called the Shadow Root. Similar to iframes, Shadow DOM doesn’t inherit styling from the parent page, which means you can create custom CSS rules within the Shadow DOM tree and not worry about messing up layout within the main page.

Here are a few other terms related to Shadow DOM that would be good to be aware of before we proceed:

Shadow Host: This is the normal DOM node (like a <div> or <button>) that contains the shadow root and all of its sub-elements. This allows the parent page to treat the element like a normal DOM node and not have to know that it’s rendering a whole set of elements inside it.

Shadow Tree: The DOM tree inside the Shadow DOM.

Shadow Boundary: The place where the shadow DOM ends, and the regular DOM begins. This can either be open, which allows the parent page to “see” the elements that are within the Shadow Tree, or closed, which prevents the parent page from seeing what elements lie within.

You can see how each of these concepts relate to each other in the diagram below, which is sourced from Mozilla’s Shadow DOM documentation. Shadow DOM

Testing Shadow DOM

Prior to Selenium 4, there was no built-in support for working with Shadow DOM elements. However, with the introduction of the getShadowRoot() function in Selenium 4, there is now a way to access Shadow DOM nodes in Selenium that does not require executing your own custom JavaScript using the JavascriptExecutor executeScript() function.

Consider the simple example of a Shadow DOM element in the screenshot below::

Shadow DOM Demo.

To create a test that accesses the Shadow DOM element containing the text “some text”, we’ll need to know a few things about the element we want to access. First, we must locate the Shadow Host (i.e. the element on the main page that contains the Shadow DOM content). Next, we’ll need to access the Shadow Root within that element. Finally, we’ll need to access the element within the Shadow Tree that we need to interact with, which in this case is the element containing some text. All of this information can be found using Chrome Developer Tools’ “Element” Pane. In the screenshot above, you can see the <div> element that the Shadow DOM is attached to. Within this element is an open ‘#shadow-root’ that contains the text element we want to test.

Note: If you’re new to Selenium, check out our guide to writing Selenium tests in Node.js.

Approach #1: Using JavascriptExecutor

To access Shadow DOM elements with JavaScript, you first need to locate and query the shadow host. Then you can use the executeScript() function to retrieve the shadow root property. Once you have access to the shadow root, you will have access to the rest of the DOM and be able to query it like the regular DOM.

Create a test file and add the following tests:

// import chromedriver so that Selenium can by itself open a chrome driver
require("chromedriver");
var assert = require("assert");

// import this class from Selenium
const { Builder, By } = require("selenium-webdriver");

(async function shadowDomTest() {
  //open chrome browser
  let driver = await new Builder().forBrowser("chrome").build();

  // go to shadow dom demo page
  await driver.get("http://watir.com/examples/shadow_dom.html");

  // get the shadow Root
  async function getShadowRootExtension() {
    let shadowHost;
    await (shadowHost = driver.findElement(By.css("#shadow_host")));
    return driver.executeScript("return arguments[0].shadowRoot", shadowHost);
  }

  // find the shadow DOM element
  async function locateShadowDomElement(shadowDomElement) {
    let shadowRoot;
    let element;
    await (shadowRoot = getShadowRootExtension());
    await shadowRoot.then(async (result) => {
      await (element = result.findElement(By.css(shadowDomElement)));
    });
    return element;
  }

  let shadowElement = await locateShadowDomElement("#shadow_content");

  // get the inner element of the shadow DOM element
  let shadowContent = await shadowElement.getText();

  // Verify the text element
  assert.equal("some text", shadowContent);
})();

The test above automates how to interact with the shadow DOM. It will verify if the content in the shadow DOM element is equal to the “some text” element on the page.

Here is an explanation of the test flow:

  1. Open the chrome browser window:

    //open chrome browser
    let driver = await new Builder().forBrowser("chrome").build();
    

    Here, we instantiate a new Chrome web driver to open a Chrome browser using the Builder class that we imported from selenium-webdriver. If you want to use another browser, such as Firefox, pass the browser name as a parameter to the forBrowser(‘firefox’) function.

  2. Go to the demo site:

    // go to shadow dom demo page
    await driver.get("http://example.com/");
    

    Navigate to the demo page.

  3. Find the shadow root element: Check the shadow root’s parent tag and get it using javascript. In this case, the shadow host tag is the div element <div id=”shadow_host">.

    async function getShadowRootExtension() {
      let shadowHost;
      await (shadowHost = driver.findElement(By.css("your_shadow_host")));
      return driver.executeScript("return arguments[0].shadowRoot", shadowHost);
    }
    

    The executeScript() function will retrieve the shadow root.

    This code above works in both Selenium 3 and 4.

  4. Using the shadow root as a driver for web elements:

    Once we have access to the shadow root, we can now use the shadow root as a driver to interact with the shadow DOM element we need. In this case, instead of locating the element on the WebDriver instance
    as before, we will locate it on the shadow root WebElement using the findElement() function :

    async function locateShadowDomElement(shadowDomElement) {
      let shadowRoot;
      let element;
      await (shadowRoot = getShadowRootExtensiont());
      await shadowRoot.then(async (result) => {
        await (element = result.findElement(By.css(shadowDomElement)));
      });
      return element;
    }
    

    Using the function locateShadowDomElement(shadowDomElement), we can now locate the shadow DOM element we need:

    let shadowElement = await locateShadowDomElement("#shadow_content");
    
  5. Verify that content in the Shadow DOM elements is equal to “some text”:

    // get the inner element of the Shadow DOM element
    let shadowContent = await shadowElement.getText();
    
    // Verify the content
    assert.equal("some text", shadowContent);
    

Note: This approach, while verbose, is compatible with versions of Selenium WebDriver prior to version 4, and works across all browsers.

Approach #2: Using the getShadowRoot() function

With this new method, you can now interact with Shadow DOM elements in Selenium 4 across all modern browsers.

Create a new test file and add the following tests:

Note: The code below requires selenium 4.

// import chromedriver so that Selenium can by itself open a chrome driver
require("chromedriver");
var assert = require("assert");

// import this class from Selenium
const { Builder, By } = require("selenium-webdriver");

(async function shadowDomTest() {
  //open chrome browser
  let driver = await new Builder().forBrowser("chrome").build();

  // go to shadow dom demo website
  await driver.get("http://watir.com/examples/shadow_dom.html");

  // get the shadowHost
  let shadowHost;
  await (shadowHost = driver.findElement(By.css("#shadow_host")));

  // find the shadow Dom element
  async function locateShadowDomElement(shadowDomElement) {
    let shadowRoot;
    let element;

    // get the shadowDow Root
    await (shadowRoot = shadowHost.getShadowRoot());
    await shadowRoot.then(async (result) => {
      await (element = result.findElement(By.css(shadowDomElement)));
    });
    return element;
  }

  let shadowElement = await locateShadowDomElement("#shadow_content");

  // get the inner element of the shadowDom element
  let shadowContent = await shadowElement.getText();

  // Verify the text element
  assert.equal("some text", shadowContent);
})();

Here are the new changes we added:

  1. Locate the shadow Host of the shadow root: In this case, the shadow Host tag is the div element <div id=”shadow_host>.

    // get the shadowHost
    let shadowHost;
    await (shadowHost = driver.findElement(By.css("#shadow_host")));
    
  2. Find the shadow DOM Element:

    // find the shadow element
    async function locateShadowDomElement(shadowDomElement) {
      let shadowRoot;
      let element;
    
      // get the shadowDow Root
      await (shadowRoot = shadowHost.getShadowRoot());
      await shadowRoot.then(async (result) => {
        await (element = result.findElement(By.css(shadowDomElement)));
      });
      return element;
    }
    

In this case, instead of using the JavaScript (executeScript()) that we used before, we can get the shadow root directly on the WebElement with the getShadowRoot() function:

await (shadowRoot = shadowHost.getShadowRoot());

With Selenium 4 and Chromium 96 or greater, shadow DOMs elements are much easier to work with without using JavaScript.

Conclusion

We have demonstrated how to automate actions that interact with shadow DOM elements in Selenium and also examined two different approaches for targeting shadow DOM elements in Selenium, using the JavascriptExecutor executeScript() function or the getShadowRoot() method introduced in Selenium 4.

Reflect: A testing tool with built-in support for Shadow DOM

Reflect is a no-code testing tool that can test virtually any action you can take on a browser. In addition to testing workflows that contain Shadow DOM, you can also test more complex actions like drag-and-drops, hovers, and file uploads. Creating a test in Reflect is easy: the tool records your actions as you use your site and automatically translates those actions into a repeatable test that you can run any time.

Get automated test coverage for your web app today — try it for free.

Get started with Reflect today

Create your first test in 2 minutes, no installation or setup required. Accelerate your testing efforts with fast and maintainable test suites without writing a line of code.

Copyright © 2022 Reflect Software Inc. All Rights Reserved.