skip to content

In Selenium, how do you reach an element nested inside two levels of shadow root?

level: middleimportance: should knowfreq 40%

answer

  1. One boundary at a time
  2. No selector spans two trees
  3. Count the round trips involved
  4. getShadowRoot at every level
  5. Element, root, element, root, element

basics

~10 s

You cross one boundary at a time: find the outer host, take its root, find the inner host inside that root, take its root, then find your element. That is five commands.

solid answer

~40 s

Each shadow boundary needs its own `getShadowRoot()` call, so the chain strictly alternates element, root, element, root, element. Find the outer custom element from the driver, call `getShadowRoot()`, find the inner custom element **from that root**, call `getShadowRoot()` on it, then search the second root for your target — five commands to get the reference, and `2n + 1` for `n` levels. `SearchContext` has no `getShadowRoot()`, so the inner host must be found first; and selectors get shorter as you descend, because each search is already scoped to that tree. A failure tells you which hop broke: `NoSuchElementException` for a host you did not find, `NoSuchShadowRootException` for an element with no root.

code

java · 11 lines
java
WebElement wizard = driver.findElement(By.cssSelector("payroll-wizard"));
SearchContext wizardRoot = wizard.getShadowRoot();

WebElement panel = wizardRoot.findElement(By.cssSelector("filing-review-panel"));
SearchContext panelRoot = panel.getShadowRoot();

for (WebElement row : panelRoot.findElements(By.cssSelector("tr.deposit-row"))) {
  System.out.println(row.getDomProperty("textContent"));
}

panelRoot.findElement(By.cssSelector("button.sign-and-file")).click();

go deeper

for a junior

Remember the shape rather than a formula: for each component you go into, find it and then take its root. Two nested components means doing that twice before you search for anything.

for a middle

Explain why the sequence strictly alternates, why SearchContext has no getShadowRoot, and why selectors get shorter as you descend into each successive tree.

for a senior

Show you cost the chain out: five to seven round trips per deep read, and know when to hoist a root before a loop versus rebuild it because a host can be replaced mid-iteration.

for a principal

Own the design conversation about how deep a suite should be willing to reach, since chain depth turns straight into command count, failure surface and triage effort for every team that writes these tests.

## One boundary per command In **Selenium 4** there is no locator that crosses a shadow boundary and no locator that crosses two. Each boundary is crossed by its own `getShadowRoot()` call, so reaching a node that sits two levels deep is a chain of alternating finds and root fetches. On a payroll-tax filing wizard, the page hosts `<payroll-wizard>`; inside its shadow tree sits `<filing-review-panel>`; inside *that* component's shadow tree sits the button that signs and files the return. The chain is: 1. `driver.findElement(By.cssSelector("payroll-wizard"))` — the outer host, found in the document. 2. `.getShadowRoot()` — the wizard's root, a `SearchContext`. 3. `wizardRoot.findElement(By.cssSelector("filing-review-panel"))` — the inner host, found **inside** the wizard's tree. 4. `.getShadowRoot()` — the panel's root. 5. `panelRoot.findElement(By.cssSelector("button.sign-and-file"))` — the target element. That is **five commands** to obtain the reference, plus a sixth to click it. The pattern generalises: *n* nested roots cost `2n + 1` commands. ## The step people skip The inner host is an ordinary element **inside the outer shadow tree**, so it must be found from the outer root — not from the driver, and not by calling `getShadowRoot()` twice on the same object. `SearchContext` has no `getShadowRoot()` at all; only a `WebElement` does. The sequence is therefore strictly alternating: element, root, element, root, element. | Attempt | Result | |---|---| | `driver.findElement(By.cssSelector("payroll-wizard filing-review-panel"))` | no match; the selector never enters the tree | | `wizardRoot.getShadowRoot()` | does not compile; `SearchContext` has no such method | | `driver.findElement(By.cssSelector("filing-review-panel"))` | no match; the inner host is not in the document tree | | `wizardRoot.findElement(...)` then `.getShadowRoot()` | correct; one boundary crossed per hop | ## Writing selectors at the right scope Each search is scoped to the tree you started from, so selectors get **shorter** as you descend, not longer. Inside the panel's root, `button.sign-and-file` is enough; qualifying it with `filing-review-panel button.sign-and-file` matches nothing, because the host element is not contained in its own shadow tree. The same applies to `findElements`: `panelRoot.findElements(By.cssSelector("tr.deposit-row"))` returns only rows in that component's tree, never rows the page renders around it. That scoping is useful in its own right — it is the tightest possible search context on the page. ## What it costs and where it bites Every hop is an HTTP round trip to the browser, and the chain is serial: you cannot ask for the second root until the first find has come back. Two consequences on a real suite: - A single deep read is cheap in absolute terms but is **five to seven round trips**, so a loop over forty wage rows that re-walks the chain each iteration turns into hundreds of commands. - Where the loop body only needs elements from one tree, hoist the chain: fetch the panel's root once before the loop, and search from it inside the loop. Refetch only when something can replace a host. ```java SearchContext wizardRoot = driver.findElement(By.cssSelector("payroll-wizard")).getShadowRoot(); SearchContext panelRoot = wizardRoot.findElement(By.cssSelector("filing-review-panel")).getShadowRoot(); for (WebElement row : panelRoot.findElements(By.cssSelector("tr.deposit-row"))) { System.out.println(row.getDomProperty("dataset")); } ``` ## Failures along the chain, and how to read them A deep chain fails at a definite step, and the exception tells you which: - `NoSuchElementException` on step 1 — the outer host is not in the document; your page-level selector or the page state is wrong. - `NoSuchShadowRootException` on step 2 or 4 — you found an element but it has no shadow root. Either you grabbed a wrapper `<div>` instead of the custom element, or that level renders into the page rather than into a tree, in which case that hop should be deleted. - `NoSuchElementException` on step 3 — the outer root exists but the inner component is not in it; check that you are searching the tree you think you are. - `DetachedShadowRootException` on a later use — a host between you and the target was replaced, so the chain must be rebuilt from the driver. Debug a broken chain **one hop at a time**, asserting that each intermediate host is found before asking it for a root. A single long expression collapses five distinguishable failures into one stack trace. ## Deciding how deep to go Depth is a property of the application, not of your test, but you still have choices about how you absorb it: - **Name the intermediate contexts.** `wizardRoot` and `panelRoot` as locals cost nothing and make the failure obvious at a glance; an anonymous five-call chain does not. - **Assert the intermediate host exists** when a chain is new or flaky, so the report names the level that broke rather than the last call in the expression. - **Check whether a level is real.** A `NoSuchShadowRootException` in the middle of a chain often means that component does not use a shadow tree at all, and the hop should be removed rather than worked around. - **Prefer the shallowest correct entry point.** If the target is reachable from the panel's root and the panel is addressable, there is no reason to walk from the wizard every time. A last practical note: because the whole chain is ordinary Selenium 4 calls on `WebElement` and `SearchContext`, it composes normally with anything that takes a search context. A helper that accepts a `SearchContext` parameter works identically whether you hand it the driver, an element or a shadow root — which is the cleanest way to keep the depth of a component out of the code that reads its fields.

  • Why can you not call getShadowRoot() on the outer root to reach the inner one?
    Because `getShadowRoot()` is declared on `WebElement`, not on `SearchContext`. The outer root is only a search context, so it offers `findElement` and `findElements` and nothing else. You must search it for the inner custom element first, and call `getShadowRoot()` on the `WebElement` that search returns.
  • How should selectors change as you descend through nested roots?
    They get shorter. Each search is already scoped to the tree you started from, so inside the panel's root `button.sign-and-file` is enough. Qualifying it with the host tag name matches nothing, because a host element is never contained in its own shadow tree. The same scoping makes `findElements` from a root return only that component's nodes.

saying these in an interview costs you the question

  • Writes one CSS selector spanning both shadow boundaries
  • Calls getShadowRoot on the shadow root itself
  • Finds the inner host from the driver instead of the outer root
  • Ignores that every hop is a separate round trip
  • Thinks nesting needs only one getShadowRoot call