Frames & Shadow DOM
Why boundaries break locators
Section titled “Why boundaries break locators”Two browser features create separate DOM worlds that normal locators cannot see into: iframes (payment forms, embedded widgets, preview panels) and Shadow DOM (web components and design systems). If your locator is correct but OrbitTest says the element does not exist, the element is very often inside one of these boundaries.
OrbitTest handles both the same way: you get a scoped object for the boundary, and inside that scope the full intent-first API works as usual.
iFrames
Section titled “iFrames”orbit.frame() accepts a locator for the iframe element and returns a scope:
const billing = await orbit.frame(orbit.getByAttribute("title", "Billing"));
await billing.type("Email", "team@example.test");await billing.click("Save billing");expect(await billing.exists("Saved")).toBe(true);Everything on the billing scope happens inside the iframe.
Nested frames (a frame inside a frame) can be resolved step by step or in one call with a path:
// Step by stepconst checkout = await orbit.frame(orbit.getByAttribute("title", "Checkout"));const vault = await checkout.frame(orbit.getByAttribute("title", "Vault"));
// Or as a pathconst vault2 = await orbit.frame([ orbit.getByAttribute("title", "Checkout"), orbit.getByAttribute("title", "Vault")]);
await vault.click("Approve");For a single action, withFrame() avoids keeping a scope variable around:
await orbit.withFrame(orbit.css("iframe.payment"), async (frame) => { await frame.click("Pay");});Shadow DOM
Section titled “Shadow DOM”orbit.shadow() works the same way, and it handles both open and closed shadow roots:
const profile = await orbit.shadow(orbit.css("user-profile"));
await profile.type("Email", "team@example.test");await profile.click("Save");expect(await profile.text(orbit.css("#status"))).toBe("Saved");Nested shadow roots chain or resolve as a path, exactly like frames:
const shell = await orbit.shadow(orbit.css("app-shell"));const panel = await shell.shadow(orbit.css("settings-panel"));await panel.click("Enable");
// Path formconst panel2 = await orbit.shadow([ orbit.css("app-shell"), orbit.css("settings-panel")]);await orbit.withShadow(orbit.css("confirm-card"), async (shadow) => { await shadow.click("Confirm");});Short-lived shadow scope for one action.
Keeping boundary code maintainable
Section titled “Keeping boundary code maintainable”- Locate the boundary by something stable: an iframe
title, a custom element tag name. Avoid finding frames by index. - Prefer
withFrame/withShadowfor one-off actions so stale scope variables cannot leak into later steps after a page navigates. - Inside a shadow root, still use user-facing locators. Reaching into a component’s internal class names couples your test to someone else’s implementation details.
- If a third-party frame (payments, captcha) is slow to load, wait for something inside it before interacting, just like a normal page.
Frequently asked questions
Section titled “Frequently asked questions”How do I know whether an element is in a frame or a shadow root?
Section titled “How do I know whether an element is in a frame or a shadow root?”In Chrome DevTools, inspect the element. If the breadcrumb shows an iframe ancestor, it is a frame; if you see #shadow-root in the tree, it is Shadow DOM.
Does a frame scope expire?
Section titled “Does a frame scope expire?”A scope refers to the frame as resolved at the time. If the page navigates or the frame reloads, resolve it again rather than reusing the old scope.
Closed shadow roots too?
Section titled “Closed shadow roots too?”Yes, orbit.shadow() works with closed roots, which normal page JavaScript cannot pierce. Use that power for testing, not for depending on private internals.
Related pages
Section titled “Related pages”- Locators — Target elements by visible text first, then by role, test ID, CSS, or XPath when needed, and which selectors to avoid.
- Working with Elements — Clicking, typing, reading text with three text readers, waiting for state, and iterating collections with orbit.all().
- Alerts & Windows — Handle alert, confirm, and prompt dialogs, notification permissions, and multi-window flows with waitForWindow.
- Troubleshooting — Fixes for unauthorized devices, missing devices, blocked Companion installs, black mirror screens, and empty log panels.