Automating Shadow DOM with Selenium WebDriver 4 in C#

How to drive web elements inside a shadow root on a web page

Courtney Zhan
3 min readAug 27

--

About 9 months ago, I published an article that documenting my first attempt on Automating Shadow DOM, using Selenium v3 in Ruby. This is an update to use Selenium v4, which added getShadowRoot() method. Below are the differences (in Java).

Selenium v3:

Using JavaScript.

WebElement shadowHost = driver.findElement(By.cssSelector("#shadow_host"));
JavascriptExecutor jsDriver = (JavascriptExecutor) driver;
WebElement shadowRoot = (WebElement) jsDriver.executeScript(
"return arguments[0].shadowRoot", shadowHost);

Selenium v4:

Get it directly.

WebElement shadowHost = driver.findElement(By.cssSelector("#shadow_host"));
WebElement shadowRoot = shadowHost.getShadowRoot();

In this article, I will show how to do the Selenium v4 way in C#. yes, another language :-), Beside a change of language (which is quite minor, really), I made a couple of improvements.

The Task

Add a new list item to the Editable List of the demo Fiddle site.

Luigi Demo Page

A typical Selenium script looks like this:

 var el= shadowRoot.FindElement(By.CssSelector("input.add-new-list-item-input"));
el.SendKeys("Peach here I come!");

But it won’t work on this site. Why? The editable list and the input text box are inside a Shadow DOM and inaccessible — for now.

Finding the Shadow DOM

Open Inspect Element in Chrome (or equivalent for other browsers).

Looking at the page, there is a stand-out tag: <luigi-wc-2f77632f6c6973742e6a73> (highlighted in the screenshot below).

Underneath it, it has one element #shadow-root (open) and that is how you can identify it. It is a bit like a frame (has a title tag …) ,except standard Selenium locators unable to find any elements within it.

Testing a web page within a Shadow DOM

  1. Get the shadow root element

Please note, the shadow-root is under the <luigi-wc-2f77632f6c6973742e6a73> , which is known as “Shadow Host”. This shadow host is a bit tricky to locate, as it conains dyanmic number (which might change in next build)

IWebElement elem = driver.FindElements(By.XPath(
"//div[contains(@class, 'wcContainer svelte-')]/*[1]"))[0];

2. shadow root element => Shadow Root (Search Context)

Then using Selenium 4’s new method.

ISearchContext shadowRoot = elem.GetShadowRoot();

3. Using Shadow Root Search Context to find and drive the elments within

 var inputElem = shadowRoot.FindElement(By.CssSelector("input.add-new-list-item-input"));
inputElem.SendKeys("Peach here I come!");
shadowRoot.FindElement(By.CssSelector("button.editable-list-add-item.icon")).Click();
System.Threading.Thread.Sleep(500);
Assert.IsTrue(driver.FindElement(By.TagName("body")).Text.Contains("Peach here I come!"));

4. Clear

In previous article, after adding a todo item I didn’t delete it. Here it is.

 var lastRemovBtn = shadowRoot.FindElement(By.CssSelector(
"div > ul > li:last-child > button"));
lastRemoveBtn.Click();

Please note, you must use the CssSelector locator, Xpath locator does not work within Shadow root.

// does not work
shadowRoot.FindElements(By.XPath("div/ul/li/button")).Last();

Ruby version

it "Drive element inside a shadow root - optimized" do
elem = driver.find_elements(:xpath, "//div[contains(@class, 'wcContainer svelte-')]/*[1]").first
shadow_root = elem.shadow_root
input_elem = shadow_root.find_element(:css, "input.add-new-list-item-input")
input_elem.send_keys("Peach here I come!")
shadow_root.find_element(:css, "button.editable-list-add-item.icon").click
sleep 0.5
expect(page_text).to include("Peach here I come!")
last_remove_btn = shadow_root.find_element(:css, "div > ul > li:last-child > button")
last_remove_btn.click
end

--

--