Shadow DOM - Workaround to create Selenium IDE Scripts

Disclaimer:

The technics described in the document are intended to workaround a Selenium IDE limitation. They may not apply to all Shadow DOM cases.

The DOM is a hierarchical  representation (tree) describing the elements (nodes) that compose a web page. This is used by the browser to render the web page (i.e. the web UI). A shadow DOM is a technology allowing to "hide" DOM trees in another DOM tree. The document tree (top level DOM) can encapsulate several shadow trees. Shadow trees themselves can encapsulate other shadow trees.

In both cases, the nodes in a shadow tree are not directly accessible from their parent tree. The parent only "sees" the shadow host node: the node the shadow root is attached to.

Impact for the Selenium IDE

To perform actions on a web application, the Selenium IDE requires to identify the "target" for the action. Yet the Selenium IDE supports different method to identify the target (XPATH, ID, CSS, ...), it always search in the document tree. If an application UI uses shadow DOM, the elements in the shadow tree won't be accessible to the Selenium IDE: it won't be able to perform actions on them.

How to Determine if an Application UI Uses Shadow DOM?

Here is a procedure to easily determine if some elements of an application web page, are in a shadow tree - i.e. not accessible to the Selenium IDE (the below explanations correspond to a Chrome browser, but most modern web browsers propose similar functionalities).

  1. Open the application in a web browser and navigate to the page displaying the element to test.
  2. Then open the "Elements" section of the browser Developer Tools.
    This displays the hierarchy of elements composing the displayed web page.
  3. In the "Elements" section, click on the "inspect" icon.
    Now, when you move the mouse cursor over the UI of the application web page, the hovered graphical element is highlighted.
  4. Move the mouse over the UI element to test and click it.
    The corresponding element node is selected in the "Elements" tree.

In the "Elements" tree hierarchy, the shadow root nodes are identified with a "#shadow-root" node. If the selected element node is under such a "#shadow-root" node, then the selected element belongs a shadow tree and the Selenium IDE won't be able to access it.

Workaround

The Selenium IDE cannot directly reach elements in a shadow tree. However, if the shadow root is open, it's possible to access such elements via a piece of Javascript.

In Javascript, it's possible to select the shadow host element and, from it, access the attached shadow root element. From the shadow root, it's then possible to access the elements in the corresponding shadow tree.

For instance (in the above sample screenshot):

document.querySelector("#shellbar").shadowRoot.querySelector("button[data-ui5-stable='menu']")

where:

  • document.querySelector("#shellbar") selects the shadow host (the element with the ID "shellbar" in the document tree).
  • shadowRoot selects the corresponding shadow root (in the above sample screenshot, we can see it is "open").
  • querySelector("button[data-ui5-stable='menu']") selects, in shadow tree, the element we want to perform an action on.

Usage in the Selenium IDE

Such a javascript expression cannot be used in most of the Selenium IDE commands.

However, the Selenium IDE has a special command to execute a piece of Javascript: the "execute script" command.
In this command, you can use the javascript expression selecting the element in the shadow tree and perform an action on it.

Some actions may not be possible but simple ones like Click or Type, are possible.

For instance, to click the button from the previous example, you can create the following Selenium IDE command:

Commandexecute script
Targetdocument.querySelector("#shellbar").shadowRoot.querySelector("button[data-ui5-stable='menu']").click();
Value 

Special case: "Wait" commands

Unlike the Selenium "wait" commands, the "execute script" command does not provide retry capability.
Therefore, if the element is not available when the command is attempted, the command will fail and the script execution will be interrupted.

A solution is to implement the wait functionality directly in the executed script.

And to make sure the rendering of the monitored web page is not blocked, the Selenium IDE command to use is "execute async script". 

Here are some possible expressions to handle "wait for element" commands.


Wait for Element P
resent

Here is an example waiting for 30s for the element to exist:

return new Promise(function(ok,ko) {

    // will try to find the element every "intervalTime" milliseconds with a maximum of "max" attempts

    const max=60;

    const intervalTime=500;

 

    let counter=0; 

    let interval = setInterval( function(){

       counter++;

        let element = null;

        // try to find the element

        try{   

           element = document.querySelector("#shellbar").shadowRoot.querySelector("button[data-ui5-stable='menu']");

        } catch(e) {};

 

        if (element != null) {

            // if found, stop the loop and indicate that the wait was successful

            clearInterval(interval);

            ok("success");

        } else {

               // if not found after "max" attempts, stop the loop and indicate that the wait failed

            if(counter >= max){

                clearInterval(interval);

                ko("failed");

            }

        };

    }, intervalTime);

}).then(arguments[arguments.length - 1],arguments[arguments.length - 1]);


Here is what would look like the Selenium IDE command to use (with a one-line Javascript expression similar to the above code -- remark: Selenium "execute" commands support multiline expression but they don't support the comment lines):

Commandexecute asynch script
Targetreturn new Promise(function(ok,ko) {const max=60;const iTime=500;let cnt=0;let i=setInterval(function(){cnt++;let elem=null;try{elem=document.querySelector("#shellbar").shadowRoot.querySelector("button[data-ui5-stable='menu']");}catch(e){};if(elem!=null){clearInterval(i);ok("success");}else{if(cnt>=max){clearInterval(i);ko("failed");}};},iTime);}).then(arguments[arguments.length-1],arguments[arguments.length-1]);
Value 

Wait for Element Visible

Here is an example waiting for 30s for the element to be visible:

 

return new Promise(function(ok,ko) {

    // will try to find the element every "intervalTime" milliseconds with a maximum of "max" attempts

    const max=60;

    const intervalTime=500;

 

    let counter=0;

    let interval = setInterval( function(){

       counter++;

        let element = null;

        // try to find the element

        try{   

           element = document.querySelector("#shellbar").shadowRoot.querySelector("button[data-ui5-stable='menu']");

        } catch(e) {};

 

        if ((element != null) && !(window.getComputedStyle(element).display === "none")) {

            // if found and is visible, stop the loop and indicate that the wait was successful

            clearInterval(interval);

            ok("success");

        } else {

               // if not found after "max" attempts, stop the loop and indicate that the wait failed

            if(counter >= max){

                clearInterval(interval);

                ko("failed");

            }

        };

    }, intervalTime);

}).then(arguments[arguments.length - 1],arguments[arguments.length - 1]);


Here is what would look like the Selenium IDE command to use (with a one-line Javascript expression similar to the above code - remark: Selenium "execute" commands support multiline expression but they don't support the comment lines):

Commandexecute asynch script
Targetreturn new Promise(function(ok,ko) {const max=60;const iTime=500;let cnt=0;let i=setInterval(function(){cnt++;let elem=null;try{elem=document.querySelector("#shellbar").shadowRoot.querySelector("button[data-ui5-stable='menu']");}catch(e){};if((elem!=null)&&!(window.getComputedStyle(elem).display==="none")){clearInterval(i);ok("success");}else{if(cnt>=max){clearInterval(i);ko("failed");}};},iTime);}).then(arguments[arguments.length-1],arguments[arguments.length-1]);
Value 

Creation of the Selenium IDE script when shadow DOM is involved

An easy way to create a Selenium IDE script is to use the recording capability.

However, the Selenium IDE cannot record actions on elements in a shadow tree.
In this case, you can:

  1. Create the beginning of your script using the recording.

  2. When you reach a shadow tree element, stop the recording.
  3. Manually append the "execute script" commands to perform the actions on the shadow tree elements

  4. If You then reach a sequence of actions that no longer involve shadow tree element, you can restart the recording from this point easily record the remaining actions.

And you can repeat the steps 2 to 4 each time a shadow tree element is involved.