.agents/skills/playwright-best-practices/browser-apis/iframes.md
// Access iframe by selector
const frame = page.frameLocator('iframe#payment')
await frame.getByRole('button', {name: 'Pay'}).click()
// Access by name attribute
const namedFrame = page.frameLocator('iframe[name="checkout"]')
await namedFrame.getByLabel('Card number').fill('4242424242424242')
// Access by title
const titledFrame = page.frameLocator('iframe[title="Payment Form"]')
// Access by src (partial match)
const srcFrame = page.frameLocator('iframe[src*="stripe.com"]')
// frameLocator - for locator-based operations (recommended)
const frameLocator = page.frameLocator('#my-iframe')
await frameLocator.getByRole('button').click()
// frame() - for Frame object operations (navigation, evaluation)
const frame = page.frame({name: 'my-frame'})
if (frame) {
await frame.goto('https://example.com')
const title = await frame.title()
}
// Get all frames
const frames = page.frames()
for (const f of frames) {
console.log('Frame URL:', f.url())
}
// Wait for iframe to load
const frame = page.frameLocator('#dynamic-iframe')
// Wait for element inside iframe
await expect(frame.getByRole('heading')).toBeVisible({timeout: 10000})
// Wait for iframe src to change
await page.waitForFunction(() => {
const iframe = document.querySelector('iframe#my-frame') as HTMLIFrameElement
return iframe?.src.includes('loaded')
})
// Cross-origin iframes work seamlessly with frameLocator
const thirdPartyFrame = page.frameLocator('iframe[src*="third-party.com"]')
// Interact with elements inside cross-origin iframe
await thirdPartyFrame.getByRole('textbox').fill('[email protected]')
await thirdPartyFrame.getByRole('button', {name: 'Submit'}).click()
// Wait for cross-origin iframe to be ready
await expect(thirdPartyFrame.locator('body')).toBeVisible()
test('Stripe payment iframe', async ({page}) => {
await page.goto('/checkout')
// Stripe uses multiple iframes for each field
const cardFrame = page.frameLocator('iframe[name*="__privateStripeFrame"]').first()
// Wait for Stripe to initialize
await expect(cardFrame.locator('[placeholder="Card number"]')).toBeVisible({
timeout: 15000,
})
// Fill card details
await cardFrame.locator('[placeholder="Card number"]').fill('4242424242424242')
await cardFrame.locator('[placeholder="MM / YY"]').fill('12/30')
await cardFrame.locator('[placeholder="CVC"]').fill('123')
})
test('OAuth iframe flow', async ({page}) => {
await page.goto('/login')
await page.getByRole('button', {name: 'Sign in with Google'}).click()
// If OAuth opens in iframe instead of popup
const oauthFrame = page.frameLocator('iframe[src*="accounts.google.com"]')
// Wait for OAuth form
await expect(oauthFrame.getByLabel('Email')).toBeVisible({timeout: 10000})
await oauthFrame.getByLabel('Email').fill('[email protected]')
})
// Parent iframe contains child iframe
const parentFrame = page.frameLocator('#outer-frame')
const childFrame = parentFrame.frameLocator('#inner-frame')
// Interact with deeply nested content
await childFrame.getByRole('button', {name: 'Submit'}).click()
// Multiple levels of nesting
const level1 = page.frameLocator('#level1')
const level2 = level1.frameLocator('#level2')
const level3 = level2.frameLocator('#level3')
await level3.getByText('Deep content').click()
// Helper to search all frames for an element
async function findInAnyFrame(page: Page, selector: string): Promise<Locator | null> {
// Check main page first
const mainCount = await page.locator(selector).count()
if (mainCount > 0) return page.locator(selector)
// Check all frames
for (const frame of page.frames()) {
const count = await frame.locator(selector).count()
if (count > 0) {
return frame.locator(selector)
}
}
return null
}
test('find element in any frame', async ({page}) => {
await page.goto('/complex-page')
const element = await findInAnyFrame(page, '[data-testid="submit-btn"]')
if (element) await element.click()
})
test('handle dynamically created iframe', async ({page}) => {
await page.goto('/dashboard')
// Click button that creates iframe
await page.getByRole('button', {name: 'Open Widget'}).click()
// Wait for iframe to appear in DOM
await page.waitForSelector('iframe#widget-frame')
// Now access the frame
const widgetFrame = page.frameLocator('#widget-frame')
await expect(widgetFrame.getByText('Widget Loaded')).toBeVisible()
})
test('iframe src changes', async ({page}) => {
await page.goto('/multi-step')
const frame = page.frameLocator('#step-frame')
// Step 1
await expect(frame.getByText('Step 1')).toBeVisible()
await frame.getByRole('button', {name: 'Next'}).click()
// Wait for iframe to reload with new content
await expect(frame.getByText('Step 2')).toBeVisible({timeout: 10000})
await frame.getByRole('button', {name: 'Next'}).click()
// Step 3
await expect(frame.getByText('Step 3')).toBeVisible({timeout: 10000})
})
test('lazy loaded iframe', async ({page}) => {
await page.goto('/page-with-lazy-iframe')
// Scroll to trigger lazy load
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
// Wait for iframe to load
const lazyFrame = page.frameLocator('#lazy-iframe')
await expect(lazyFrame.locator('body')).not.toBeEmpty({timeout: 15000})
// Interact with content
await lazyFrame.getByRole('button').click()
})
test('iframe internal navigation', async ({page}) => {
await page.goto('/app')
// Get frame object for navigation control
const frame = page.frame({name: 'content-frame'})
if (!frame) throw new Error('Frame not found')
// Navigate within iframe
await frame.goto('https://embedded-app.com/page2')
// Wait for navigation
await frame.waitForURL('**/page2')
// Verify content
await expect(frame.getByRole('heading')).toHaveText('Page 2')
})
test('track iframe navigation', async ({page}) => {
const navigations: string[] = []
// Listen to frame navigation
page.on('framenavigated', (frame) => {
if (frame.parentFrame()) {
// This is an iframe navigation
navigations.push(frame.url())
}
})
await page.goto('/with-iframe')
await page.frameLocator('#nav-frame').getByRole('link', {name: 'Page 2'}).click()
// Verify navigation occurred
expect(navigations.some((url) => url.includes('page2'))).toBe(true)
})
// fixtures.ts
import {test as base, FrameLocator} from '@playwright/test'
export const test = base.extend<{paymentFrame: FrameLocator}>({
paymentFrame: async ({page}, use) => {
await page.goto('/checkout')
// Wait for payment iframe to be ready
const frame = page.frameLocator('iframe[src*="payment"]')
await expect(frame.locator('body')).toBeVisible({timeout: 15000})
await use(frame)
},
})
// test file
test('complete payment', async ({paymentFrame}) => {
await paymentFrame.getByLabel('Card').fill('4242424242424242')
await paymentFrame.getByRole('button', {name: 'Pay'}).click()
})
test('debug iframe content', async ({page}) => {
await page.goto('/page-with-iframes')
// List all frames
console.log('All frames:')
for (const frame of page.frames()) {
console.log(` - ${frame.name() || '(unnamed)'}: ${frame.url()}`)
}
// Screenshot specific iframe content
const frame = page.frame({name: 'target-frame'})
if (frame) {
const body = frame.locator('body')
await body.screenshot({path: 'iframe-content.png'})
}
// Get iframe HTML for debugging
const frameContent = page.frameLocator('#my-frame')
const html = await frameContent.locator('body').innerHTML()
console.log('iFrame HTML:', html.substring(0, 500))
})
test('handle iframe load failure', async ({page}) => {
await page.goto('/page-with-unreliable-iframe')
const frame = page.frameLocator('#unreliable-frame')
try {
// Try to interact with iframe content
await expect(frame.getByRole('button')).toBeVisible({timeout: 5000})
await frame.getByRole('button').click()
} catch (error) {
// Fallback: refresh iframe
await page.evaluate(() => {
const iframe = document.querySelector('#unreliable-frame') as HTMLIFrameElement
if (iframe) iframe.src = iframe.src
})
// Retry
await expect(frame.getByRole('button')).toBeVisible({timeout: 10000})
await frame.getByRole('button').click()
}
})
test('mock iframe response', async ({page}) => {
// Intercept iframe src request
await page.route('**/embedded-widget**', (route) => {
route.fulfill({
contentType: 'text/html',
body: `
<!DOCTYPE html>
<html>
<body>
<h1>Mocked Widget</h1>
<button>Mocked Button</button>
</body>
</html>
`,
})
})
await page.goto('/page-with-widget')
const frame = page.frameLocator('#widget-frame')
await expect(frame.getByRole('heading')).toHaveText('Mocked Widget')
})
| Anti-Pattern | Problem | Solution |
|---|---|---|
Using page.frame() for interactions | Less reliable than frameLocator | Use page.frameLocator() for element interactions |
| Hardcoding iframe index | Fragile if DOM order changes | Use name, id, or src attribute selectors |
| Not waiting for iframe load | Race conditions | Wait for element inside iframe to be visible |
| Assuming same-origin | Cross-origin has different timing | Always wait for iframe content explicitly |
| Ignoring nested iframes | Element not found | Chain frameLocator calls for nested frames |