# Frames and shadow DOM

> Find and check elements inside browser frames and open shadow roots.

Source: https://rehearsal.dev/retest/docs/guides/frames-and-shadow-dom

If a control sits inside an iframe or a web component, your locator may need a different scope. Name a frame explicitly; ordinary locators already search through open shadow roots.

## Find elements in a frame

An iframe has its own document, which page locators do not enter. Pass a locator for its `iframe` or `frame` element to `page.frame()`. This example assumes your task form is embedded in an iframe titled "Tasks".

```ts filename="tests/frames.retest.ts"
import { expect, test } from '@rehearsal-labs/retest'

test('saves a task inside a frame', async ({ page }) => {
  await page.goto('/')
  const tasks = page.frame(page.locator('iframe[title="Tasks"]'))
  await tasks.getByTestId('task-title').fill('Release checklist')
  await tasks.getByTestId('save-task').click()
  await expect(tasks.getByTestId('saved-task')).toHaveText('Release checklist')
})
```

The returned frame has the same finders as a page, scoped to its document. Use `tasks.getByTestId('task-title')` for the embedded field and `page` for elements around the frame.

`frame()` creates a scope without reading the document, so it needs no `await`. The frame element is found again on every action and check, including after the app replaces it.

## Nested and cross-origin frames

To enter a nested frame, make its element locator from the frame that contains it. If the task editor embeds a Preview frame, you can check the saved title inside that second document.

```ts
const preview = tasks.frame(tasks.locator('iframe[title="Preview"]'))
await expect(preview.getByTestId('saved-task')).toHaveText('Release checklist')
```

The `iframe` locator is made from `tasks`, so it names an element inside the parent frame. Passing a locator made on `page` to `tasks.frame()` would fail with `usage`, because it belongs to a different document scope.

Frames from another origin work through the same API, including ones the browser runs in another process. If you fill a secret there, the frame's origin must be among the secret's [allowed destinations](https://rehearsal.dev/retest/docs/guides/signing-in.md).

A frame has no `goto()` of its own, and URL and title checks belong to the page. If the frame navigates after a form submission, its next locator searches the new document.

## Frame failures and limits

A missing frame is waited for within the action budget, then fails with `not_found` and names the frame locator. Several matching frame elements fail with `ambiguous`; an element that is not a frame fails with `usage`.

Retest checks both the element inside the frame and the frames around it before sending input. A hidden frame or an overlay covering it can make the action fail with `not_actionable`, even if its own document shows the button.

Frame actions and checks work on Chromium-family browsers. Firefox, WebKit and Electron refuse frame-scoped locators with `unsupported`, while native apps have no browser frames.

> [!NOTE]
> **Operations inside a frame**
>
> `setInputFiles()` and element screenshot comparisons inside a frame are `unsupported`. Catch its file picker through `page.fileChooser()` instead. Layout checks cannot compare elements in different frames and fail with `usage` if you try.

Frame steps name their own document in the report, but failure screenshots show the page viewport. A frame below that viewport may be outside the picture because checks do not scroll it into view.

## Find elements in shadow DOM

Shadow DOM holds a web component's internal markup. If you put the task form inside an open shadow root, its test can keep the same locators. Test ids, text, placeholders and CSS search through nested open roots too.

```ts filename="tests/shadow-dom.retest.ts"
import { expect, test } from '@rehearsal-labs/retest'

test('saves a task rendered by a component', async ({ page }) => {
  await page.goto('/')
  await page.getByTestId('task-title').fill('Release checklist')
  await page.getByTestId('save-task').click()
  await expect(page.getByTestId('saved-task')).toHaveText('Release checklist')
})
```

The example searches the page and its open shadow roots with no special shadow selector. You can also scope a finder inside a component locator when several components show the same control.

CSS also crosses open root boundaries: `page.locator('task-editor button.primary')` can find a primary button inside that component. Use ordinary CSS without a `>>>` separator; [Locators](https://rehearsal.dev/retest/docs/essentials/locators.md) explains how to narrow a query with several matches.

## Shadow DOM support

On Chromium-family browsers and Electron, role and label locators also reach open roots through the browser's accessibility tree. A closed shadow root is out of reach on every engine, so Retest finds no elements inside it.

Firefox and WebKit support test id, text, placeholder and CSS queries through open roots. A role or label query fails with `unsupported` when an open root in its search could hold a match; the failure names the component host.

If you hit that refusal, use a test id or another supported finder for the element inside the root. Shadow DOM is a web feature; native app elements use their platform's locators.

## Next steps

- [Locators](https://rehearsal.dev/retest/docs/essentials/locators.md): Choose a finder and narrow queries to one element.
- [Uploads and downloads](https://rehearsal.dev/retest/docs/guides/files.md): Handle file pickers opened by a frame.
- [Browsers](https://rehearsal.dev/retest/docs/platforms/browsers.md): Check which features each browser supports.
