10

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" name="bday">

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.

surfmuggle
  • 5,527
  • 7
  • 48
  • 77
alecxe
  • 462,703
  • 120
  • 1,088
  • 1,195
  • Are you using the last version of ChromeDriver? Chromedriver supports Shadow DOM since v2.14 (2015-01-28). – f.cipriani Mar 12 '15 at 12:08
  • @f.cipriani yes, the latest (2.14). Thanks. – alecxe Mar 12 '15 at 21:24
  • Do you have a _functional_ example of the use case? – SiKing Mar 12 '15 at 22:31
  • @SiKing I've been using [the one from w3schools](http://www.w3schools.com/html/tryit.asp?filename=tryhtml_input_date) (sorry :)). Thanks. Note: you would need to switch to an iframe before accessing the date input. – alecxe Mar 12 '15 at 22:32
  • Works only in Chrome, not in FF. :( Are you looking for python-only answer? – SiKing Mar 12 '15 at 22:35
  • @SiKing if you would provide an answer using other selenium bindings it can also help - I'll try to mimic the solution and see if I can reach that picker button with python bindings. Thanks. – alecxe Mar 12 '15 at 22:37

2 Answers2

11

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.

f.cipriani
  • 3,357
  • 2
  • 26
  • 22
  • This makes total sense. You are basically saying that this new chromedriver feature: [Issue 852: Support shadow dom in chromedriver.](https://code.google.com/p/chromedriver/issues/detail?id=852) is basically about custom elements with shadow DOM inside, correct? – alecxe Mar 12 '15 at 23:30
  • 3
    That's correct. Chromedriver is just a wrapper on the top of the browser API. Since Chrome doesn't allow scriptable access to user-agent shadow DOMs (they can be styled tho), the same is true for selenium. – f.cipriani Mar 13 '15 at 15:14
  • great answer, but it has some downsides for nested shadow roots elements check my answer below – Eduard Florinescu May 16 '16 at 11:21
9

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()
Eduard Florinescu
  • 16,747
  • 28
  • 113
  • 179
  • 3
    His only works if the Shadow DOM is open, correct? Otherwise the .shadowRoot will return null. Is there a way to access the closed Shadow DOM? Writing a Chrome Extension wouldn't be a problem – Pascal Feb 28 '19 at 14:01
  • @Pascal .shadowRoot returns the Shadow DOM, open and close is just in the Inspector GUI, if something returns null and you expect that element to be a shadow root you might have selected the wrong element – Eduard Florinescu Feb 28 '19 at 14:10
  • 1
    Thank you for the reply. In this blog post, it exemplifies the fact that closed Shadow DOM returns NULL (https://blog.revillweb.com/open-vs-closed-shadow-dom-9f3d7427d1af). – Pascal Feb 28 '19 at 14:26
  • @Pascal Actually you might be right I was unaware of this open/close modes – Eduard Florinescu Feb 28 '19 at 15:09
  • Yeah, I didn't know either. I found out during this Selenium test case, and I'm trying to find a way around it. Tks anyways. – Pascal Feb 28 '19 at 15:37
  • @Pascal Yeah, it seems that you need to inject this before that page JavaScript runs `Element.prototype._attachShadow = Element.prototype.attachShadow; Element.prototype.attachShadow = function () { return this._attachShadow( { mode: "open" } ); };` – Eduard Florinescu Feb 28 '19 at 18:12
  • https://stackoverflow.com/questions/31354352/selenium-how-to-inject-execute-a-javascript-in-to-a-page-before-loading-executi – Eduard Florinescu Feb 28 '19 at 18:30
  • Or you could make an extension that runs that before and then run Selenium with extensions see: https://stackoverflow.com/a/48564477/1577343 – Eduard Florinescu Feb 28 '19 at 18:32
  • 1
    Yeah... I'll probably go with the extension. Thanks so much – Pascal Feb 28 '19 at 21:57
  • Not that easy... couldn't get it to work. https://stackoverflow.com/questions/54954383/override-element-prototype-attachshadow-using-chrome-extension – Pascal Mar 02 '19 at 01:33
  • @Pascal Glad to see that some one figured it out that is a different context, good to learn something upvoted both – Eduard Florinescu Mar 02 '19 at 07:18