skip to content

What do Playwright's locator.contentFrame() and frameLocator.owner() do?

level: middleimportance: should knowfreq 38%

answer

  1. One iframe, two things to address
  2. The element outside, the document inside
  3. A conversion in each direction
  4. Assertions need the element form back

basics

~20 s

They convert between the two views of an iframe. locator.contentFrame() turns a Locator for the iframe element into a FrameLocator for the document inside it, and frameLocator.owner() turns a FrameLocator back into a Locator for the iframe element.

solid answer

~40 s

An iframe has two addresses: the element in the parent document, held by a `Locator`, and the document inside it, held by a `FrameLocator`. `locator.contentFrame()` goes from element to contents, `frameLocator.owner()` goes back -- both added in Playwright 1.43 and current in 1.63. You need `contentFrame()` whenever you already hold a locator rather than a raw selector, which is also the supported way to pick one of several embeds: `page.locator('iframe.preview').nth(1).contentFrame()` replaces the deprecated `FrameLocator.nth()`. You need `owner()` because web-first assertions take a `Locator`, so asserting the embed itself is visible, reading its `src`, or scrolling it into view all go through it. Both hops are lazy: nothing is queried until the chain is used.

code

typescript · 9 lines
typescript
const previews = page.locator('iframe.preview');
await expect(previews).toHaveCount(2);

// Locator -> FrameLocator: work inside the second preview.
const second = previews.nth(1).contentFrame();
await second.getByRole('link', { name: 'Open issue' }).click();

// FrameLocator -> Locator: assert on the iframe element itself.
await expect(second.owner()).toBeVisible();

go deeper

for a junior

Remember the pair as a two-way street: contentFrame goes into the embedded document, owner comes back out to the iframe element itself.

for a middle

Explain why both types exist and that each conversion is lazy, then show the indexing form locator.nth(i).contentFrame() that replaces the deprecated frame locator indexers.

for a senior

Use owner() diagnostically: assert the embed rendered before chasing a failure inside it, so a missing vendor iframe reports as a missing iframe rather than a mystery timeout.

for a principal

Decide what your helpers hand around. Passing one frame-scoped object that can be both asserted on and stepped into keeps embed churn contained instead of leaking selectors into every test.

## An iframe has two addresses Any embedded document on an issue tracker -- the comment composer, a preview pane, a partner board widget -- can be talked about in two different ways, and Playwright gives each its own type: - the **`<iframe>` element** living in the parent document, addressed by a `Locator`: you can count it, assert it is visible, screenshot it, scroll it into view, read its `src`; - the **document inside it**, addressed by a `FrameLocator`: you chain element locators onto it and they resolve within that document. The two conversion methods are the bridge between those views, both added in Playwright 1.43 and current in 1.63: | Call | Direction | Returns | |---|---|---| | `locator.contentFrame()` | element -> contents | a `FrameLocator` for the document that iframe hosts | | `frameLocator.owner()` | contents -> element | a `Locator` for the `<iframe>` element itself | ## Why contentFrame() exists `page.frameLocator(selector)` already enters an iframe from a raw selector. `contentFrame()` matters when you have a **locator** rather than a selector -- because you built it, filtered it, indexed it, or were handed it by a helper. ```ts const previews = page.locator('iframe.preview'); await expect(previews).toHaveCount(2); await previews.nth(1).contentFrame().getByRole('link', { name: 'Open issue' }).click(); ``` That indexing form is also the supported way to disambiguate several matching iframes. `FrameLocator.first()`, `last()` and `nth()` still exist but are **deprecated**: the documented replacement is `locator.nth(i).contentFrame()`, which keeps all the indexing on the `Locator` side where the rest of the API lives. ## Why owner() exists Web-first assertions take a `Locator`, not a `FrameLocator`. When the thing under test is the embed itself, `owner()` is how you get back to it: ```ts const composer = page.frameLocator('iframe[title="Comment composer"]'); await expect(composer.owner()).toBeVisible(); await composer.owner().scrollIntoViewIfNeeded(); ``` Typical uses: 1. asserting the embed rendered at all before blaming its contents for a failure, 2. reading an attribute on the iframe element, such as the `src` a lazy loader swapped in, 3. taking a screenshot bounded to the embed rather than the whole issue view, 4. handing a helper one object it can both assert on and step into. ## Round trips and what stays lazy `page.locator('#composer').contentFrame().owner()` addresses the same element as `page.locator('#composer')`. Neither hop performs a query: both types are lazy descriptions, so a round trip costs nothing at build time and every use re-resolves the selector against the live DOM. That is why holding a `FrameLocator` across a re-render is safe in a way that holding a captured element is not. ## Choosing between them - Reaching **into** the embed from a plain selector: `page.frameLocator(selector)` is the shortest path. - Reaching **into** the embed from a locator you already narrowed: `contentFrame()`. - Asserting or acting **on** the embed element: `owner()`, or simply keep the original iframe locator around. - Picking one of several matching embeds: index the `Locator`, then convert -- `page.locator('iframe.preview').nth(1).contentFrame()`. ## Common mistakes - Passing a `FrameLocator` to `expect()` and expecting a visibility assertion; the matcher wants the `Locator` that `owner()` returns. - Reading `owner()` as "the parent frame". It is the iframe element this frame locator points at, not the frame above it. - Reaching for the deprecated `frameLocator.nth(1)` instead of `locator.nth(1).contentFrame()`. - Calling `owner()` on a frame locator built by the selector-less `page.frameLocator()`, which points at no particular iframe and does not support it.

  • The issue detail page renders three preview iframes. How do you drive the second one in Playwright 1.63?
    Index on the locator side and then convert: `page.locator('iframe.preview').nth(1).contentFrame()`. `FrameLocator.first()`, `last()` and `nth()` still work but are deprecated, and the documented replacement is exactly this `locator.nth(i).contentFrame()` form.
  • Why can you not pass a FrameLocator straight to expect()?
    Web-first matchers such as `toBeVisible()` describe an element, and a `FrameLocator` describes a document view rather than an element. `owner()` converts it to the `Locator` for the `<iframe>` element, which the matcher accepts and auto-retries against.

saying these in an interview costs you the question

  • Calling owner() the parent frame rather than the iframe element
  • Asserting visibility directly on a FrameLocator
  • Using the deprecated frameLocator.nth() to disambiguate embeds
  • Believing contentFrame() performs a query and returns a promise
  • Thinking contentFrame() and page.frameLocator() return different types