Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
browser-apis/iframes.md
1# iFrame Testing23## Table of Contents451. [Basic iFrame Access](#basic-iframe-access)62. [Cross-Origin iFrames](#cross-origin-iframes)73. [Nested iFrames](#nested-iframes)84. [Dynamic iFrames](#dynamic-iframes)95. [iFrame Navigation](#iframe-navigation)106. [Common Patterns](#common-patterns)1112## Basic iFrame Access1314### Using frameLocator1516```typescript17// Access iframe by selector18const frame = page.frameLocator("iframe#payment");19await frame.getByRole("button", { name: "Pay" }).click();2021// Access by name attribute22const namedFrame = page.frameLocator('iframe[name="checkout"]');23await namedFrame.getByLabel("Card number").fill("4242424242424242");2425// Access by title26const titledFrame = page.frameLocator('iframe[title="Payment Form"]');2728// Access by src (partial match)29const srcFrame = page.frameLocator('iframe[src*="stripe.com"]');30```3132### Frame vs FrameLocator3334```typescript35// frameLocator - for locator-based operations (recommended)36const frameLocator = page.frameLocator("#my-iframe");37await frameLocator.getByRole("button").click();3839// frame() - for Frame object operations (navigation, evaluation)40const frame = page.frame({ name: "my-frame" });41if (frame) {42await frame.goto("https://example.com");43const title = await frame.title();44}4546// Get all frames47const frames = page.frames();48for (const f of frames) {49console.log("Frame URL:", f.url());50}51```5253### Waiting for iFrame Content5455```typescript56// Wait for iframe to load57const frame = page.frameLocator("#dynamic-iframe");5859// Wait for element inside iframe60await expect(frame.getByRole("heading")).toBeVisible({ timeout: 10000 });6162// Wait for iframe src to change63await page.waitForFunction(() => {64const iframe = document.querySelector("iframe#my-frame") as HTMLIFrameElement;65return iframe?.src.includes("loaded");66});67```6869## Cross-Origin iFrames7071### Accessing Cross-Origin Content7273```typescript74// Cross-origin iframes work seamlessly with frameLocator75const thirdPartyFrame = page.frameLocator('iframe[src*="third-party.com"]');7677// Interact with elements inside cross-origin iframe78await thirdPartyFrame.getByRole("textbox").fill("[email protected]");79await thirdPartyFrame.getByRole("button", { name: "Submit" }).click();8081// Wait for cross-origin iframe to be ready82await expect(thirdPartyFrame.locator("body")).toBeVisible();83```8485### Payment Provider iFrames (Stripe, PayPal)8687```typescript88test("Stripe payment iframe", async ({ page }) => {89await page.goto("/checkout");9091// Stripe uses multiple iframes for each field92const cardFrame = page93.frameLocator('iframe[name*="__privateStripeFrame"]')94.first();9596// Wait for Stripe to initialize97await expect(cardFrame.locator('[placeholder="Card number"]')).toBeVisible({98timeout: 15000,99});100101// Fill card details102await cardFrame103.locator('[placeholder="Card number"]')104.fill("4242424242424242");105await cardFrame.locator('[placeholder="MM / YY"]').fill("12/30");106await cardFrame.locator('[placeholder="CVC"]').fill("123");107});108```109110### Handling OAuth in iFrames111112```typescript113test("OAuth iframe flow", async ({ page }) => {114await page.goto("/login");115await page.getByRole("button", { name: "Sign in with Google" }).click();116117// If OAuth opens in iframe instead of popup118const oauthFrame = page.frameLocator('iframe[src*="accounts.google.com"]');119120// Wait for OAuth form121await expect(oauthFrame.getByLabel("Email")).toBeVisible({ timeout: 10000 });122await oauthFrame.getByLabel("Email").fill("[email protected]");123});124```125126## Nested iFrames127128### Accessing Nested Frames129130```typescript131// Parent iframe contains child iframe132const parentFrame = page.frameLocator("#outer-frame");133const childFrame = parentFrame.frameLocator("#inner-frame");134135// Interact with deeply nested content136await childFrame.getByRole("button", { name: "Submit" }).click();137138// Multiple levels of nesting139const level1 = page.frameLocator("#level1");140const level2 = level1.frameLocator("#level2");141const level3 = level2.frameLocator("#level3");142await level3.getByText("Deep content").click();143```144145### Finding Elements Across Frame Hierarchy146147```typescript148// Helper to search all frames for an element149async function findInAnyFrame(150page: Page,151selector: string,152): Promise<Locator | null> {153// Check main page first154const mainCount = await page.locator(selector).count();155if (mainCount > 0) return page.locator(selector);156157// Check all frames158for (const frame of page.frames()) {159const count = await frame.locator(selector).count();160if (count > 0) {161return frame.locator(selector);162}163}164return null;165}166167test("find element in any frame", async ({ page }) => {168await page.goto("/complex-page");169const element = await findInAnyFrame(page, '[data-testid="submit-btn"]');170if (element) await element.click();171});172```173174## Dynamic iFrames175176### iFrames Created at Runtime177178```typescript179test("handle dynamically created iframe", async ({ page }) => {180await page.goto("/dashboard");181182// Click button that creates iframe183await page.getByRole("button", { name: "Open Widget" }).click();184185// Wait for iframe to appear in DOM186await page.waitForSelector("iframe#widget-frame");187188// Now access the frame189const widgetFrame = page.frameLocator("#widget-frame");190await expect(widgetFrame.getByText("Widget Loaded")).toBeVisible();191});192```193194### iFrames with Changing src195196```typescript197test("iframe src changes", async ({ page }) => {198await page.goto("/multi-step");199200const frame = page.frameLocator("#step-frame");201202// Step 1203await expect(frame.getByText("Step 1")).toBeVisible();204await frame.getByRole("button", { name: "Next" }).click();205206// Wait for iframe to reload with new content207await expect(frame.getByText("Step 2")).toBeVisible({ timeout: 10000 });208await frame.getByRole("button", { name: "Next" }).click();209210// Step 3211await expect(frame.getByText("Step 3")).toBeVisible({ timeout: 10000 });212});213```214215### Lazy-Loaded iFrames216217```typescript218test("lazy loaded iframe", async ({ page }) => {219await page.goto("/page-with-lazy-iframe");220221// Scroll to trigger lazy load222await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));223224// Wait for iframe to load225const lazyFrame = page.frameLocator("#lazy-iframe");226await expect(lazyFrame.locator("body")).not.toBeEmpty({ timeout: 15000 });227228// Interact with content229await lazyFrame.getByRole("button").click();230});231```232233## iFrame Navigation234235### Navigating Within iFrame236237```typescript238test("iframe internal navigation", async ({ page }) => {239await page.goto("/app");240241// Get frame object for navigation control242const frame = page.frame({ name: "content-frame" });243if (!frame) throw new Error("Frame not found");244245// Navigate within iframe246await frame.goto("https://embedded-app.com/page2");247248// Wait for navigation249await frame.waitForURL("**/page2");250251// Verify content252await expect(frame.getByRole("heading")).toHaveText("Page 2");253});254```255256### Handling Frame Navigation Events257258```typescript259test("track iframe navigation", async ({ page }) => {260const navigations: string[] = [];261262// Listen to frame navigation263page.on("framenavigated", (frame) => {264if (frame.parentFrame()) {265// This is an iframe navigation266navigations.push(frame.url());267}268});269270await page.goto("/with-iframe");271await page272.frameLocator("#nav-frame")273.getByRole("link", { name: "Page 2" })274.click();275276// Verify navigation occurred277expect(navigations.some((url) => url.includes("page2"))).toBe(true);278});279```280281## Common Patterns282283### iFrame Fixture284285```typescript286// fixtures.ts287import { test as base, FrameLocator } from "@playwright/test";288289export const test = base.extend<{ paymentFrame: FrameLocator }>({290paymentFrame: async ({ page }, use) => {291await page.goto("/checkout");292293// Wait for payment iframe to be ready294const frame = page.frameLocator('iframe[src*="payment"]');295await expect(frame.locator("body")).toBeVisible({ timeout: 15000 });296297await use(frame);298},299});300301// test file302test("complete payment", async ({ paymentFrame }) => {303await paymentFrame.getByLabel("Card").fill("4242424242424242");304await paymentFrame.getByRole("button", { name: "Pay" }).click();305});306```307308### Debugging iFrame Issues309310```typescript311test("debug iframe content", async ({ page }) => {312await page.goto("/page-with-iframes");313314// List all frames315console.log("All frames:");316for (const frame of page.frames()) {317console.log(` - ${frame.name() || "(unnamed)"}: ${frame.url()}`);318}319320// Screenshot specific iframe content321const frame = page.frame({ name: "target-frame" });322if (frame) {323const body = frame.locator("body");324await body.screenshot({ path: "iframe-content.png" });325}326327// Get iframe HTML for debugging328const frameContent = page.frameLocator("#my-frame");329const html = await frameContent.locator("body").innerHTML();330console.log("iFrame HTML:", html.substring(0, 500));331});332```333334### Handling iFrame Load Failures335336```typescript337test("handle iframe load failure", async ({ page }) => {338await page.goto("/page-with-unreliable-iframe");339340const frame = page.frameLocator("#unreliable-frame");341342try {343// Try to interact with iframe content344await expect(frame.getByRole("button")).toBeVisible({ timeout: 5000 });345await frame.getByRole("button").click();346} catch (error) {347// Fallback: refresh iframe348await page.evaluate(() => {349const iframe = document.querySelector(350"#unreliable-frame",351) as HTMLIFrameElement;352if (iframe) iframe.src = iframe.src;353});354355// Retry356await expect(frame.getByRole("button")).toBeVisible({ timeout: 10000 });357await frame.getByRole("button").click();358}359});360```361362### Mocking iFrame Content363364```typescript365test("mock iframe response", async ({ page }) => {366// Intercept iframe src request367await page.route("**/embedded-widget**", (route) => {368route.fulfill({369contentType: "text/html",370body: `371<!DOCTYPE html>372<html>373<body>374<h1>Mocked Widget</h1>375<button>Mocked Button</button>376</body>377</html>378`,379});380});381382await page.goto("/page-with-widget");383384const frame = page.frameLocator("#widget-frame");385await expect(frame.getByRole("heading")).toHaveText("Mocked Widget");386});387```388389## Anti-Patterns to Avoid390391| Anti-Pattern | Problem | Solution |392| ------------------------------------- | --------------------------------- | -------------------------------------------------- |393| Using `page.frame()` for interactions | Less reliable than frameLocator | Use `page.frameLocator()` for element interactions |394| Hardcoding iframe index | Fragile if DOM order changes | Use name, id, or src attribute selectors |395| Not waiting for iframe load | Race conditions | Wait for element inside iframe to be visible |396| Assuming same-origin | Cross-origin has different timing | Always wait for iframe content explicitly |397| Ignoring nested iframes | Element not found | Chain frameLocator calls for nested frames |398399## Related References400401- **Locators**: See [locators.md](../core/locators.md) for selector strategies402- **Third-party services**: See [third-party.md](../advanced/third-party.md) for payment iframe patterns403- **Debugging**: See [debugging.md](../debugging/debugging.md) for troubleshooting iframe issues404