mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 17:22:38 +02:00
11 KiB
11 KiB
iFrame Testing
Table of Contents
- Basic iFrame Access
- Cross-Origin iFrames
- Nested iFrames
- Dynamic iFrames
- iFrame Navigation
- Common Patterns
Basic iFrame Access
Using frameLocator
// 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"]');
Frame vs FrameLocator
// 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());
}
Waiting for iFrame Content
// 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
Accessing Cross-Origin Content
// 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("test@example.com");
await thirdPartyFrame.getByRole("button", { name: "Submit" }).click();
// Wait for cross-origin iframe to be ready
await expect(thirdPartyFrame.locator("body")).toBeVisible();
Payment Provider iFrames (Stripe, PayPal)
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");
});
Handling OAuth in iFrames
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("test@gmail.com");
});
Nested iFrames
Accessing Nested Frames
// 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();
Finding Elements Across Frame Hierarchy
// 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();
});
Dynamic iFrames
iFrames Created at Runtime
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();
});
iFrames with Changing src
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 });
});
Lazy-Loaded iFrames
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();
});
iFrame Navigation
Navigating Within iFrame
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");
});
Handling Frame Navigation Events
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);
});
Common Patterns
iFrame Fixture
// 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();
});
Debugging iFrame Issues
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));
});
Handling iFrame Load Failures
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();
}
});
Mocking iFrame Content
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-Patterns to Avoid
| 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 |
Related References
- Locators: See locators.md for selector strategies
- Third-party services: See third-party.md for payment iframe patterns
- Debugging: See debugging.md for troubleshooting iframe issues