Shadow DOM Maze
Playwright's locators pierce open shadow roots as if they were not there, right up until they cannot. Four levels: a plain open root, shadow nested inside shadow with slotted content, a closed root (the wall), and the boss room, an iframe living inside a shadow root. First time with shadow DOM? Start on the basic shadow DOM widget.
4Levels
3Custom elements
1Closed root
1Iframe boss
L1 Open shadow root
<tta-subscribe> renders its form inside an open shadow root. Normal locators walk straight in.
Goal: fill the email inside the component and click Subscribe with plain
getByRole / getByPlaceholder, then assert the confirmation line. No special shadow API needed.L2 Shadow inside shadow, plus slots
<tta-panel> (shadow) wraps a nested <tta-subscribe> (its own shadow) and a slotted light-DOM note.
Goal: subscribe inside the NESTED component, and separately assert the slotted note text. Notice both resolve with ordinary locators; nesting depth is irrelevant for open roots.
L3 The closed root
<tta-vault> attaches its shadow with mode: "closed". Its internals are invisible to every locator engine.
Goal: prove the wall exists: a locator for the button INSIDE the vault times out. Then use the escape hatches real apps provide: the host's
data-count attribute mirrors the hidden counter, and the visible summary line below the component updates. Assert those instead.Vault clicks so far: 0
L4 Boss: iframe inside a shadow root
<tta-embed> hosts an iframe (srcdoc) inside its open shadow root. Two boundaries stacked.
Goal: reach the button inside the frame inside the shadow. Use
page.frameLocator('iframe#inner-embed') (the shadow boundary is pierced to find the iframe, the frame boundary needs frameLocator), click Approve, and assert the frame's status text.Drills
- Level 1: subscribe with
page.getByPlaceholder('[email protected]')and assert "Subscribed" appears. Zero shadow-specific code. - Level 2: scope with
page.getByTestId('level2-widget')and subscribe inside it; assert the slotted note withgetByTestId('level2-slotted-note'). - Level 3: write the failing locator first, catch the timeout, then assert
data-countflips to 3 after three clicks usingexpect(locator).toHaveAttribute(). Click through the vault's visible host area. - Level 4: chain
frameLocatorthrough the shadow-hosted iframe and complete the approval, asserting on both sides of the frame boundary. - Bonus: run
locator.count()forbuttonacross the whole page and explain which buttons the number misses, and why.