7

I am working on an automation project which uses shadow DOMs extensively. I use the execute_script function to access shadow root elements.

For example:

root = driver.execute_script('return document.querySelector(".flex.vertical.layout").shadowRoot')

Then I use the root element to access the elements within. Since we have shadow root elements at many levels, this is annoying me a lot. Is there any better solution to access elements within shadow root elements?

I am using Chrome 2.20 driver.

upe
  • 1,862
  • 1
  • 19
  • 33
Arav
  • 163
  • 1
  • 1
  • 10
  • I read once in the release notes (around a year ago) that Chrome binary driver started supporting shadow-dom in selenium. Not sure what was really implemented. – djangofan Mar 21 '16 at 22:35
  • Apparently so - though the question is what bindings are available to let you use it. Certainly no friendly API (friendlier than the OP's code) within the Selenium code AFAICT. – Andrew Regan Mar 21 '16 at 23:15
  • I gave an example in the answe how to tackle the nested shadow roots – Eduard Florinescu May 16 '16 at 11:07

7 Answers7

5

By googling I found another workaround for this problem - which is using the "/deep/ combinator".

For example, I was able to access all the shadow roots elements with

driver.find_elements_by_css_selector('body/deep/.layout.horizontal.center')

This will have access to the element with the compound class name layout horizontal center regardless of the number of shadow roots it has.

But this only works for the chromedriver and /deep/ is a deprecated approach.

upe
  • 1,862
  • 1
  • 19
  • 33
Arav
  • 163
  • 1
  • 1
  • 10
3

The WebDriver spec still doesn't have anything specific to say about Shadow DOM.

Nor the Selenium project pages - which is understandable, as they closely follow the spec. Yet there is some low-level code in existence.

So, the short answer is: no, there is no special support in the spec, in Selenium's WebDriver API or implementation code at present.

Yes, the capability seems to exist in ChromeDriver 2.14 (as a wrapper around Chrome). However, as far as I can tell there are no Selenium or WebDriver-level bindings to let you use it.

But for more detail and possible workarounds, see also: Accessing Shadow DOM tree with Selenium, also: Accessing elements in the shadow DOM, and especially: Finding elements in the shadow DOM

Community
  • 1
  • 1
Andrew Regan
  • 5,087
  • 6
  • 37
  • 73
2

Trying to have this automated on Chrome I came up with an inelegant solution of recursively searching through each shadow dom explicitly using:

driver.executeScript(scriptToRun, cssSelector);

Here's the javascript (passed as a string):

function recursiveSearch(element, target) {
    let result = element.querySelector(target);
    if (result) { return result; }
    let subElements = element.querySelectorAll("*");
    for (let i = 0; i < subElements.length; i++) {
        let subElement = subElements[i];
        if (subElement && subElement.shadowRoot) {
            let result = recursiveSearch(subElement.shadowRoot, target);
            if (result) return result;
        }
    }
}
return recursiveSearch(document, arguments[0]);

Since the contents of a shadowRoot might be empty initially one can use driver.wait and until.elementIsVisible to avoid returning a null element.

Async example:

return await driver.wait(until.elementIsVisible(await driver.wait(async () => {
           return await driver.executeScript(scriptToRun, cssSelector);
       }, timeOut)));

Alternatively

My previous solution was to traverse the elements with shadowdoms explicitly, but is less autonomous. Same as above but with this script:

let element = document.querySelector(arguments[0][0]);
let selectors = arguments[0].slice(1);
for (i = 0; i < selectors.length; i++) {
    if (!element || !element.shadowRoot) {return false;}
        element = element.shadowRoot.querySelector(selectors[i]);
    }
return element;

Where selectors would be something like:

['parentElement1', 'parentElement2', 'targetElement']

Sidenote

I found that running my automation tests on Firefox Quantum 57.0 doesn't suffer from hidden shadow doms, and any element can be found with a simple:

driver.findElement(searchQuery);
upe
  • 1,862
  • 1
  • 19
  • 33
Beyers
  • 21
  • 2
2

You can write extension methods to operate on IWebElement to expand the root as below.

public static class SeleniumExtension
{
    public static IWebElement ExpandRootElement(this IWebElement element, IWebDriver driver)
    {
        return (IWebElement)((IJavaScriptExecutor)driver)
                .ExecuteScript("return arguments[0].shadowRoot", element);
    }
}

You can use the above extension method to traverse through the element's hierarchy to reach the element of interest.

By downloads_manager_ShadowDom= By.TagName("downloads-manager");
By downloadToolBarShadowDom = By.CssSelector("downloads-toolbar");
By toolBarElement = By.CssSelector("cr-toolbar");

IWebElement ToolBarElement = driver.FindElement(downloads_manager_ShadowDom).ExpandRootElement(driver).FindElement(downloadToolBarShadowDom).ExpandRootElement(driver).FindElement(toolBarElement);
upe
  • 1,862
  • 1
  • 19
  • 33
Gitesh P
  • 21
  • 2
0

Since you use often that you may create a function, then the above becomes:

def select_shadow_element_by_css_selector(selector):
  running_script = 'return document.querySelector("%s").shadowRoot' % selector
  element = driver.execute_script(running_script)
  return element

shadow_section = select_shadow_element_by_css_selector(".flex.vertical.layout")
shadow_section.find_element_by_css(".flex")

on the resulting element you can put any of the methods:

find_element_by_id
find_element_by_name
find_element_by_xpath
find_element_by_link_text
find_element_by_partial_link_text
find_element_by_tag_name
find_element_by_class_name
find_element_by_css_selector

To find multiple elements (these methods will return a list):

find_elements_by_name
find_elements_by_xpath
find_elements_by_link_text
find_elements_by_partial_link_text
find_elements_by_tag_name
find_elements_by_class_name
find_elements_by_css_selector

later edit:

Sometime the shadow host elements are hidden withing shadow trees that's why the best way to do it is to use the selenium selectors to find the shadow host elements and inject the script just to take the shadow root: :

def expand_shadow_element(element):
  shadow_root = driver.execute_script('return arguments[0].shadowRoot', element)
  return shadow_root

#the above becomes 
shadow_section = expand_shadow_element(find_element_by_tag_name("neon-animatable"))
shadow_section.find_element_by_css(".flex")

To put this into perspective I just added a testable example with Chrome's download page, clicking the search button needs open 3 nested shadow root elements:

import selenium
from selenium import webdriver
driver = webdriver.Chrome()


def expand_shadow_element(element):
  shadow_root = driver.execute_script('return arguments[0].shadowRoot', element)
  return shadow_root

selenium.__file__
driver.get("chrome://downloads")
root1 = driver.find_element_by_tag_name('downloads-manager')
shadow_root1 = expand_shadow_element(root1)

root2 = shadow_root1.find_element_by_css_selector('downloads-toolbar')
shadow_root2 = expand_shadow_element(root2)

root3 = shadow_root2.find_element_by_css_selector('cr-search-field')
shadow_root3 = expand_shadow_element(root3)

search_button = shadow_root3.find_element_by_css_selector("#search-button")
search_button.click()
Eduard Florinescu
  • 16,747
  • 28
  • 113
  • 179
  • shadow_root = driver.execute_script('return arguments[0].shadowRoot', element) What does this line do? Why arguments[0] instead of element? – Vincent Aug 03 '17 at 12:11
  • @Vincent that line of code (`return arguments[0].shadowRoot`) is in JavaScript, and arguments[0] is the way to pass values from python to JavaScript when executing driver.execute_script , the value is that of the element indeed but this is the way to do it. – Eduard Florinescu Aug 03 '17 at 14:15
0

Maybe you may use IJavaScriptExecutor?

IWebDriver driver;
IJavaScriptExecutor jsExecutor = (IJavaScriptExecutor)driver;
jsExecutor.ExecuteScript('yourShadowDom.func()');
upe
  • 1,862
  • 1
  • 19
  • 33
jmbmage
  • 2,487
  • 3
  • 27
  • 44
0

Not sure it works in all browsers, but for me ::shadow works fine in chromedriver 2.38 For example:

div::shadow div span::shadow a
Xotabu4
  • 3,063
  • 17
  • 29