Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Accessing elements in the shadow DOM

Is it possible to find elements inside the Shadow DOM with python-selenium?

Example use case:

I have this input with type="date":

<input type="date">

And I'd like to click the date picker button on the right and choose a date from the calendar.

If you would inspect the element in Chrome Developer Tools and expand the shadow-root node of the date input, you would see the button is appearing as:

<div pseudo="-webkit-calendar-picker-indicator" id="picker"></div>

Screenshot demonstrating how it looks in Chrome:

enter image description here

Finding the "picker" button by id results into NoSuchElementException:

>>> date_input = driver.find_element_by_name('bday')
>>> date_input.find_element_by_id('picker')
...
selenium.common.exceptions.NoSuchElementException: Message: no such element

I've also tried to use ::shadow and /deep/ locators as suggested here:

>>> driver.find_element_by_css_selector('input[name=bday]::shadow #picker')
...
selenium.common.exceptions.NoSuchElementException: Message: no such element
>>>
>>> driver.find_element_by_css_selector('input[name=bday] /deep/ #picker')
...
selenium.common.exceptions.NoSuchElementException: Message: no such element

Note that I can change the date in the input by sending keys to it:

driver.find_element_by_name('bday').send_keys('01/11/2014')

But, I want to set the date specifically by choosing it from a calendar.

like image 887
alecxe Avatar asked Mar 07 '15 05:03

alecxe


People also ask

What are shadow DOM elements?

Shadow DOM allows hidden DOM trees to be attached to elements in the regular DOM tree — this shadow DOM tree starts with a shadow root, underneath which you can attach any element, in the same way as the normal DOM.

How do you interact with the shadow DOM root element?

To access the shadow DOM elements using JavaScript you first need to query the shadow host element and then can access its shadowRoot property. Once you have access to the shadowRoot , you can query the rest of the DOM like regular JavaScript. var host = document. getElementById('shell'); var root = host.


2 Answers

There's no way to access the shadow root of native HTML 5 elements.

Not useful in this case, but with Chrome it's possible to access a custom created shadow root:

var root = document.querySelector("#test_button").createShadowRoot();
root.innerHTML = "<button id='inner_button'>Button in button</button"
<button id="test_button"></button>

The root can then be accessed this way:

 var element = document.querySelector("#test_button").shadowRoot;

If you want to automate a click on the inner button with selenium python (chromedriver version 2.14+):

 >>> outer = driver.execute_script('return document.querySelector("#test_button").shadowRoot')
 >>> inner = outer.find_element_by_id("inner_button")
 >>> inner.click()

Update 9 Jun 2015

This is the link to the current Shadow DOM W3C Editor's draft on github:

http://w3c.github.io/webcomponents/spec/shadow/

If you're interested in browsing the blink source code, this is a good starting point.

like image 194
f.cipriani Avatar answered Oct 19 '22 22:10

f.cipriani


The accepted answer has a drawback, many times 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 accepted answer code then becomes 
outer = expand_shadow_element(driver.find_element_by_css_selector("#test_button"))
inner = outer.find_element_by_id("inner_button")
inner.click()

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: enter image description here

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

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()

Doing the same using the accepted answer approach has the drawback that it hard-codes the queries, is less readable and you cannot use the intermediary selections for other actions:

search_button = driver.execute_script('return document.querySelector("downloads-manager").shadowRoot.querySelector("downloads-toolbar").shadowRoot.querySelector("cr-search-field").shadowRoot.querySelector("#search-button")')
search_button.click()
like image 40
Eduard Florinescu Avatar answered Oct 19 '22 23:10

Eduard Florinescu