Modern web applications frequently use embedded content, new tabs and multiple browser pages. Automation engineers need to identify which browser object they are interacting with.
A normal browser tab is represented by a Playwright
Page.
An iframe is represented by a Playwright
Frame.
Multiple tabs normally mean multiple
Page objects inside the same browser context.
Suppose a payment application embeds a payment form inside an iframe.
<iframe
src="/payment-form"
title="Payment Form">
</iframe>
The fields inside the iframe belong to the iframe document, not directly to the main page.
Using frame locator
payment_frame = page.frame_locator(
'iframe[title="Payment Form"]'
)
payment_frame.get_by_label(
"Card Number"
).fill("4111111111111111")
payment_frame.get_by_label(
"Expiry"
).fill("12/30")
payment_frame.get_by_label(
"CVV"
).fill("123")
frame_locator() allows you to locate elements
inside an iframe without manually switching the browser
context like traditional window switching.
A common real-world application is an e-commerce checkout page where the payment provider renders card fields inside an iframe.
from playwright.sync_api import expect
def test_checkout_payment(page):
page.goto(
"https://example.test/checkout"
)
payment_frame = page.frame_locator(
'iframe[title="Payment Form"]'
)
payment_frame.get_by_label(
"Card Number"
).fill("4111111111111111")
payment_frame.get_by_label(
"Expiry"
).fill("12/30")
payment_frame.get_by_label(
"CVV"
).fill("123")
payment_frame.get_by_role(
"button",
name="Pay"
).click()
expect(
page.get_by_text(
"Payment successful"
)
).to_be_visible()
The frame locator can be chained to interact with elements inside the frame.
frame = page.frame_locator(
"#customer-frame"
)
frame.get_by_label(
"First Name"
).fill("Rahul")
frame.get_by_label(
"Last Name"
).fill("Sharma")
frame.get_by_role(
"button",
name="Save"
).click()
Main page locator:
page.get_by_label(...)
iframe locator:
page.frame_locator(...).get_by_label(...)
Sometimes an iframe does not have a useful title or stable CSS selector. You can locate it using a URL pattern.
payment_frame = page.frame_locator(
'iframe[src*="payment"]'
)
payment_frame.get_by_label(
"Card Number"
).fill("4111111111111111")
title, data-testid or a stable
application-specific attribute when available.
Avoid relying on a long generated CSS/XPath expression.
Playwright also exposes the frames currently attached to a page.
for frame in page.frames:
print(
"Frame URL:",
frame.url
)
print(
"Frame name:",
frame.name
)
This is particularly useful while debugging applications containing several iframes.
If the application has a named frame, you can inspect the available frames and select the appropriate one.
payment_frame = page.frame(
name="paymentFrame"
)
assert payment_frame is not None
payment_frame.get_by_label(
"Card Number"
).fill("4111111111111111")
Use
frame_locator() when you can identify the
iframe with a stable locator. It keeps the interaction style
close to normal Playwright locator usage.
In Playwright, a browser tab is represented by a
Page.
Main Tab
New Tab
Another Tab
You normally do not need to perform a traditional "switch-to-window" operation.
A new page can be created directly from the browser context.
context = page.context
new_page = context.new_page()
new_page.goto(
"https://example.test/reports"
)
print(new_page.url)
context.new_page() creates another page in the
same browser context. It shares the context's session state,
subject to how that context was configured.
Imagine an admin application where the administrator keeps the dashboard open and opens a report in another tab.
from playwright.sync_api import expect
def test_admin_report(page):
page.goto(
"https://example.test/admin"
)
with page.expect_popup() as popup_info:
page.get_by_role(
"link",
name="Open Sales Report"
).click()
report_page = popup_info.value
expect(
report_page.get_by_role(
"heading",
name="Sales Report"
)
).to_be_visible()
expect(
report_page
).to_have_title(
"Sales Report"
)
print(
"Report URL:",
report_page.url
)
If a click is expected to open another page, use
expect_popup() around the action.
with page.expect_popup() as popup_info:
page.get_by_role(
"link",
name="Open Invoice"
).click()
invoice_page = popup_info.value
invoice_page.wait_for_load_state()
print(invoice_page.url)
The code explicitly connects the user action with the new page event expected from that action.
Opening the popup is not enough. A professional test should validate the business result.
with page.expect_popup() as popup_info:
page.get_by_role(
"link",
name="Download Invoice"
).click()
invoice_page = popup_info.value
expect(
invoice_page
).to_have_title(
"Invoice"
)
expect(
invoice_page.get_by_text(
"Invoice Number"
)
).to_be_visible()
assert "invoice" in (
invoice_page.url.lower()
)
You can keep references to multiple pages and interact with them independently.
context = page.context
customer_page = context.new_page()
admin_page = context.new_page()
customer_page.goto(
"https://example.test/customer"
)
admin_page.goto(
"https://example.test/admin"
)
print(
"Customer:",
customer_page.url
)
print(
"Admin:",
admin_page.url
)
You can inspect pages currently available in the browser context.
for browser_page in page.context.pages:
print(
"Page URL:",
browser_page.url
)
This can be useful for debugging applications that unexpectedly open additional tabs.
Suppose an agent opens a customer portal and a support portal in separate tabs.
from playwright.sync_api import expect
def test_customer_and_support_portals(page):
context = page.context
customer_page = context.new_page()
support_page = context.new_page()
customer_page.goto(
"https://example.test/customer"
)
support_page.goto(
"https://example.test/support"
)
expect(
customer_page.get_by_role(
"heading",
name="Customer Portal"
)
).to_be_visible()
expect(
support_page.get_by_role(
"heading",
name="Support Portal"
)
).to_be_visible()
Real applications can combine frames and multiple pages. For example, a checkout page can open a payment provider in a new tab, where the payment form itself is inside an iframe.
from playwright.sync_api import expect
def test_external_payment(page):
page.goto(
"https://example.test/checkout"
)
with page.expect_popup() as popup_info:
page.get_by_role(
"button",
name="Open Payment"
).click()
payment_page = popup_info.value
payment_frame = payment_page.frame_locator(
'iframe[title="Payment Form"]'
)
payment_frame.get_by_label(
"Card Number"
).fill("4111111111111111")
payment_frame.get_by_label(
"Expiry"
).fill("12/30")
payment_frame.get_by_label(
"CVV"
).fill("123")
payment_frame.get_by_role(
"button",
name="Pay"
).click()
expect(
payment_page.get_by_text(
"Payment successful"
)
).to_be_visible()
Main Page → Popup Page → Frame → Element
Selenium traditionally requires switching the driver context into the iframe before interacting with elements inside it.
from selenium.webdriver.common.by import By
iframe = driver.find_element(
By.CSS_SELECTOR,
'iframe[title="Payment Form"]'
)
driver.switch_to.frame(iframe)
driver.find_element(
By.ID,
"cardNumber"
).send_keys(
"4111111111111111"
)
driver.find_element(
By.ID,
"expiry"
).send_keys(
"12/30"
)
driver.find_element(
By.ID,
"cvv"
).send_keys(
"123"
)
driver.switch_to.default_content()
switch_to.frame() changes the current
driver context. After completing the iframe operation, you often
need to switch back using default_content().
For nested frames, Selenium can switch into each frame sequentially.
outer_frame = driver.find_element(
By.ID,
"outerFrame"
)
driver.switch_to.frame(
outer_frame
)
inner_frame = driver.find_element(
By.ID,
"innerFrame"
)
driver.switch_to.frame(
inner_frame
)
driver.find_element(
By.ID,
"customerName"
).send_keys(
"Rahul"
)
driver.switch_to.default_content()
Selenium uses window handles to identify different browser windows or tabs.
main_window = (
driver.current_window_handle
)
driver.find_element(
By.ID,
"openReport"
).click()
windows = driver.window_handles
for window in windows:
if window != main_window:
driver.switch_to.window(
window
)
break
print(
"Popup URL:",
driver.current_url
)
main_window = (
driver.current_window_handle
)
# Switch to popup
driver.switch_to.window(
popup_window
)
# Work with popup
print(driver.title)
# Return to original tab
driver.switch_to.window(
main_window
)
| Task | Playwright | Selenium |
|---|---|---|
| Locate iframe |
page.frame_locator()
|
Locate iframe element first. |
| Interact inside iframe | Use frame locator directly. |
driver.switch_to.frame()
|
| Return to main document | Continue using page locator. |
driver.switch_to.default_content()
|
| Nested frames | Chain frame locators. | Switch through frames sequentially. |
| Task | Playwright | Selenium |
|---|---|---|
| Browser tab |
Page
|
Window/tab represented by a window handle. |
| Open new page |
context.new_page()
|
Usually create/open a new browser tab/window and work with handles. |
| Popup generated by click |
page.expect_popup()
|
Inspect window_handles.
|
| Switch to another tab |
Use the corresponding Page object.
|
driver.switch_to.window()
|
| Return to original tab |
Continue using the original Page.
|
Switch back using the original window handle. |
Scenario: An administrator clicks Open Customer Report. A new tab opens and the report must be validated.
Playwright
with page.expect_popup() as popup_info:
page.get_by_role(
"link",
name="Open Customer Report"
).click()
report_page = popup_info.value
expect(
report_page.get_by_role(
"heading",
name="Customer Report"
)
).to_be_visible()
Selenium
main_window = (
driver.current_window_handle
)
driver.find_element(
By.ID,
"customerReport"
).click()
for window in driver.window_handles:
if window != main_window:
driver.switch_to.window(
window
)
break
assert (
"Customer Report"
in driver.title
)
| Area | Playwright | Selenium |
|---|---|---|
| iframe |
frame_locator()
|
switch_to.frame()
|
| Main document after iframe | Continue using page locators. |
switch_to.default_content()
|
| New tab |
New Page
|
Window handle |
| Popup created by click |
expect_popup()
|
Inspect window handles |
| Switch tab | Use the page reference. |
switch_to.window()
|
| Multiple pages |
Multiple Page objects.
|
Multiple window handles. |
| Browser context isolation |
Built-in BrowserContext.
|
Usually managed through separate driver sessions or other test infrastructure. |
| Event-based popup handling | Dedicated popup event API. | Commonly handled through window handles. |
❌ Avoid
page.get_by_role(
"link",
name="Open Report"
).click()
page.wait_for_timeout(3000)
# Search through pages and hope the popup is ready
✅ Preferred
with page.expect_popup() as popup_info:
page.get_by_role(
"link",
name="Open Report"
).click()
report_page = popup_info.value
expect(
report_page.get_by_role(
"heading",
name="Report"
)
).to_be_visible()
❌ Avoid
driver.find_element(
"xpath",
"/html/body/div[3]/iframe"
)
✅ Preferred
payment_frame = page.frame_locator(
'iframe[title="Payment Form"]'
)
payment_frame.get_by_label(
"Card Number"
).fill(
"4111111111111111"
)
Here is a realistic workflow combining:
- Main checkout page.
- External payment provider opened in a new tab.
- Payment form inside an iframe.
- Payment submission.
- Return/validation of payment result.
from playwright.sync_api import expect
def test_complete_checkout(page):
# --------------------------------
# 1. Open checkout
# --------------------------------
page.goto(
"https://example.test/checkout"
)
# --------------------------------
# 2. Open payment provider
# --------------------------------
with page.expect_popup() as popup_info:
page.get_by_role(
"button",
name="Pay Securely"
).click()
payment_page = popup_info.value
# --------------------------------
# 3. Find payment iframe
# --------------------------------
payment_frame = (
payment_page
.frame_locator(
'iframe[title="Payment Form"]'
)
)
# --------------------------------
# 4. Fill payment details
# --------------------------------
payment_frame.get_by_label(
"Card Number"
).fill(
"4111111111111111"
)
payment_frame.get_by_label(
"Expiry"
).fill(
"12/30"
)
payment_frame.get_by_label(
"CVV"
).fill(
"123"
)
# --------------------------------
# 5. Submit payment
# --------------------------------
payment_frame.get_by_role(
"button",
name="Pay"
).click()
# --------------------------------
# 6. Validate result
# --------------------------------
expect(
payment_page.get_by_text(
"Payment successful"
)
).to_be_visible()
| Mistake | Preferred Approach |
|---|---|
| Trying to locate iframe elements from the main page. |
Use frame_locator().
|
Using time.sleep() before a popup.
|
Use expect_popup().
|
| Treating a new tab as a JavaScript dialog. |
Handle it as a new Page.
|
| Using Selenium-style window switching in Playwright. |
Keep references to the relevant Playwright
Page objects.
|
| Using unstable iframe XPath. | Prefer stable iframe attributes. |
| Only verifying that a tab opened. | Validate title, URL or business content. |
| Creating shared mutable state between tabs/tests. | Keep test data and browser contexts isolated. |
| Requirement | Playwright Python |
|---|---|
| Find iframe |
page.frame_locator("iframe")
|
| Access named frame |
page.frame(name="paymentFrame")
|
| List frames |
page.frames
|
| Current page |
page
|
| Create new page |
page.context.new_page()
|
| Wait for popup |
page.expect_popup()
|
| Get popup page |
popup_info.value
|
| List open pages |
page.context.pages
|
| Popup URL |
popup_page.url
|
-
Use
frame_locator()for stable iframe interactions. - Prefer semantic or stable iframe locators over generated XPath.
-
Treat each browser tab as a Playwright
Page. -
Use
expect_popup()when a specific action is expected to create a new page. - Keep references to pages instead of repeatedly searching through all open pages.
- Validate popup title, URL or meaningful business content.
-
Avoid arbitrary
wait_for_timeout()calls for synchronization. - Keep browser contexts isolated when tests represent independent users.
- Close pages/contexts through fixtures or test lifecycle management when they are explicitly created.
- Remember that an iframe and a new browser tab are two different automation concepts.
expect_popup()
frame_locator()
iframe →
page.frame_locator()
New tab created by click →
page.expect_popup()
New page created directly →
page.context.new_page()
Multiple existing tabs → Keep references to their
Page objects.
Frame →
frame_locator()
Popup →
expect_popup()
New Tab →
Page
Multiple Tabs → Multiple
Page objects
Selenium → Frame switching + Window Handles