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.
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.
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');
});
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();
}
}
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")
Written by PV
© 2026 All Rights Reserved