Shadow DOM in Ruby Selenium

What changed in Chromium 96 and what to do about it

Titus Fortner
3 min readDec 17, 2021
Ruby logo with Selenium logo in Shadow
Selenium logo in Shadow with Ruby

Selenium users are frustrated that Chromium v96 has broken their shadow root code. The change is a feature, not a bug! Here’s how to locate Shadow DOM elements for each browser and version in Ruby.

Note: versions of this article are also available in: Java, .NET and Python

In Chromium v96, shadow root values were made compliant with the W3C WebDriver specification. The spec now includes definitions for getting an element’s shadow root and locating elements in a shadow root. This applies to all Chromium browsers (Chrome, Edge, Opera, etc), with implementations in Firefox and Safari expected soon.

If you’ve been working with shadow DOM elements in Ruby, this is the code you’ve been using:

shadow_host = driver.find_element(css: '#shadow_host')
script = 'return arguments[0].shadowRoot'
shadow_root = driver.execute_script(script, shadow_host)
shadow_content = shadow_root.find_element(css: 'shadow_content')

(see full example on GitHub)

That code works for Chromium browsers before v96 and Safari in both Selenium 3 and Selenium 4. Selenium looks at return values from the Driver#execute_script command, and if it recognizes an element, it converts it automatically. As of Chromium v96, the JavaScript returns something that can’t be converted to an element, and the above code results in this error:

NoMethodError: undefined method `find_element' for #<Hash:0x00007fdc69997800>

Unlike Java, Ruby didn’t consider this use case when Selenium 4.0 was released, but it is fixed in Selenium 4.1 and will work as expected for all versions of Chrome, Edge and Safari.

If you’re only interested in executing against the latest versions of Chrome or Edge, there’s a better way to do this, though. Introducing the new Element#shadow_root method.

shadow_host = driver.find_element(css: '#shadow_host')
shadow_root = shadow_host.shadow_root
shadow_content = shadow_root.find_element(css: '#shadow_content')

(see full example on GitHub)

Firefox. Firefox is different. Firefox currently doesn’t give you access to the shadow root element directly, but does let you get the children of the root. You’ll need to do something like this:

shadow_host = driver.find_element(css: '#shadow_host')
script = 'return arguments[0].shadowRoot.children'
children = driver.execute_script(script, shadow_host)
shadow_content = children.first do |child|
child.attribute('id') == 'shadow_content'
end

(see full example on GitHub)

What about you crazy kids who don’t want to upgrade to Selenium 4? What do you do? Well, to start with, upgrade to Selenium 4 already, it’s actually really easy for most users and if you do it now, it’ll save you headaches in the future. Regardless, I have a delightfully hacky option for you if you just have to get Shadow DOM elements in Selenium 3:

shadow_host = driver.find_element(css: '#shadow_host')script = 'return arguments[0].shadowRoot'
shadow_root_hash = driver.execute_script(script, shadow_host)
id = shadow_root_hash['shadow-6066-11e4-a52e-4f735466cecf']
bridge = driver.send(:bridge)
shadow_root = Selenium::WebDriver::Element.new(bridge, id)
shadow_content = shadow_root.find_element(css: '#shadow_content')

(see full example on GitHub)

Additional References:
* Language-agnostic version of this article on my website,
* Video with Java examples on YouTube

Now you know how to locate shadow DOM elements in all the browser versions and Selenium versions. Happy testing!

--

--

Titus Fortner

A (mostly Ruby) open source developer (Selenium, Watir, etc); currently work @saucelabs; passionate about digital confidence & improving test automation success