Intermediate Chapter 9 · 11 min read

Working with Frames & Iframes

Access content inside iframes and nested frames using Playwright's frameLocator API with full assertion support.

Why Iframes Are Tricky

Iframes embed a separate browsing context inside a page. Traditional testing tools require you to switch context into the frame, interact, and then switch back — a stateful and error-prone workflow. Playwright handles iframes differently: the frameLocator() method returns a locator scoped to the frame's document, so you can chain further locators inside it exactly as you would on the main page, without any context switching.

Tip: If a frame is served from a different domain, Playwright handles it transparently without any special configuration — cross-origin iframes work the same as same-origin ones.

Using frameLocator

page.frameLocator(selector) accepts any CSS or XPath selector that points to an <iframe> element. You can then call any locator method on the result — getByRole, getByLabel, locator() — and Playwright automatically resolves them inside the frame. All web-first assertions work normally inside frame locators, so you can expect(...).toBeVisible() on an element inside an iframe without any extra steps.

Nested Frames

For pages that embed a frame within a frame, simply chain frameLocator() calls. The resulting locator is scoped to the innermost frame. This composition model scales to arbitrary nesting depth without adding complexity to your test code.

frames.spec.js
import { test, expect } from '@playwright/test';

test('interact with content inside an iframe', async ({ page }) => {
  await page.goto('/embed-demo');

  // Scope locators to the iframe by CSS selector
  const frame = page.frameLocator('iframe[title="Payment Form"]');

  // Use any locator method inside the frame
  await frame.getByLabel('Card number').fill('4111 1111 1111 1111');
  await frame.getByLabel('Expiry').fill('12/28');
  await frame.getByLabel('CVC').fill('123');

  // Assertions work exactly as on the main page
  await expect(frame.getByText('Secured by Stripe')).toBeVisible();

  // Click a button inside the frame
  await frame.getByRole('button', { name: 'Pay Now' }).click();

  // Back on the main page — assert success message
  await expect(page.getByText('Payment successful')).toBeVisible();
});

test('nested frames', async ({ page }) => {
  await page.goto('/nested-frames');

  // Chain frameLocator calls for nested frames
  const innerFrame = page
    .frameLocator('#outer-frame')
    .frameLocator('#inner-frame');

  await expect(innerFrame.getByRole('heading')).toHaveText('Deep Content');
});
FramesTest.java
import com.microsoft.playwright.*;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;

class FramesTest {
  void iframeInteraction(Page page) {
    page.navigate("http://localhost:3000/embed-demo");

    // Scope all subsequent locators to the iframe
    FrameLocator frame = page
        .frameLocator("iframe[title='Payment Form']");

    frame.getByLabel("Card number").fill("4111 1111 1111 1111");
    frame.getByLabel("Expiry").fill("12/28");
    frame.getByLabel("CVC").fill("123");

    assertThat(frame.getByText("Secured by Stripe")).isVisible();

    frame.getByRole(AriaRole.BUTTON,
        new FrameLocator.GetByRoleOptions().setName("Pay Now")).click();

    assertThat(page.getByText("Payment successful")).isVisible();
  }
}
test_frames.py
from playwright.sync_api import Page, expect

def test_iframe_interaction(page: Page):
    page.goto("/embed-demo")

    # All locators inside this variable are scoped to the iframe
    frame = page.frame_locator("iframe[title='Payment Form']")

    frame.get_by_label("Card number").fill("4111 1111 1111 1111")
    frame.get_by_label("Expiry").fill("12/28")
    frame.get_by_label("CVC").fill("123")

    expect(frame.get_by_text("Secured by Stripe")).to_be_visible()
    frame.get_by_role("button", name="Pay Now").click()
    expect(page.get_by_text("Payment successful")).to_be_visible()

def test_nested_frames(page: Page):
    page.goto("/nested-frames")

    # Chain frame_locator for nested frames
    inner_frame = (
        page
        .frame_locator("#outer-frame")
        .frame_locator("#inner-frame")
    )
    expect(inner_frame.get_by_role("heading")).to_have_text("Deep Content")

Playwright Intermediate Working with Frames & Iframes

Written by PV

© 2026 All Rights Reserved