DEV Community

Cover image for 🥷 Shadow DOM in Test Automation: A Practical Guide with Cypress
Daniil
Daniil

Posted on

🥷 Shadow DOM in Test Automation: A Practical Guide with Cypress

When testing web apps, the DOM is not always as straightforward as it looks.
Sometimes the element you “see” in DevTools isn’t accessible to your test. Why? Because it’s wrapped inside iframes or shadow DOM.

In this article, let’s break down what shadow DOM is, why it matters, and how you can test it with Cypress.

What is Shadow DOM?

Shadow DOM is an encapsulated subtree of the DOM that lives inside a host element.

It was introduced as part of the Web Components spec and is widely used in modern UI libraries (Material UI, Ionic, Lit, Angular components).

Key benefits:

  • CSS and JS are scoped (no global leaks)
  • Components are reusable
  • Structure is hidden from the “main” DOM

Think of it as a mini DOM inside the DOM.

Types of Shadow DOM

Normal DOM (baseline)

<div id="login"> <input id="username" /> </div> 
Enter fullscreen mode Exit fullscreen mode

Cypress:

cy.get('#username').type('hello') 
Enter fullscreen mode Exit fullscreen mode

Shadow DOM (open)

<my-login> #shadow-root (open) <input id="username" /> </my-login> 
Enter fullscreen mode Exit fullscreen mode

Option A: Use .shadow() in Cypress

cy.get('my-login') .shadow() .find('#username') .type('hello') 
Enter fullscreen mode Exit fullscreen mode

Option B: Enable globally in cypress.config.js

module.exports = { e2e: { includeShadowDom: true } } 
Enter fullscreen mode Exit fullscreen mode

Shadow DOM (closed)

<my-login> # — - > shadow-root (closed) <input id="username" /> </my-login> 
Enter fullscreen mode Exit fullscreen mode

⚠️ Closed shadow roots cannot be accessed with Cypress or regular query selectors.
They are designed to be private.
Workaround: you’ll need to use component-level testing, or rely on app exposing test hooks (e.g. props or attributes).

Slotted Elements

Sometimes elements are “projected” into a shadow DOM via a slot:

<my-login> #shadow-root (open) <slot name="email"></slot> </my-login> <input slot="email" id="email-input" /> 
Enter fullscreen mode Exit fullscreen mode

Custom Cypress command:

Cypress.Commands.add('getSlottedElement', (selector) => { return cy.get(selector).then(($el) => { return cy.wrap($el[0].assignedNodes()[0]) }) }) 
Enter fullscreen mode Exit fullscreen mode

Usage

cy.getSlottedElement('#email-input').type('hello@qa.com') 
Enter fullscreen mode Exit fullscreen mode

Why Shadow DOM Matters in Automation

  • It’s everywhere in modern SPAs and Web Components
  • Your tests may fail with “element not found” unless you account for it
  • Handling shadow DOM properly makes tests stable and future-proof

Remember that:

  • DOM ≠ always accessible elements
  • Web apps can include: plain DOM, iframes, Shadow DOM (open/closed)
  • Cypress supports open shadow DOM with .shadow() or includeShadowDom config property
  • For slotted elements, you may need custom helpers. .shadow() can’t see slotted content directly, because it lives outside the shadow root and is just projected into it
  • For closed shadow DOM, collaborate with devs for test hooks

If you’re new to automation:
Start experimenting with Cypress and a simple Web Component demo.
You’ll quickly see why handling
shadow DOM is a must have skill.

Top comments (0)