Native Select Dropdowns vs Custom Dropdown Widgets
selectOption() works directly against a real <select> element by setting its value programmatically, without needing to simulate opening a dropdown UI at all. Many modern web apps, however, build their own dropdown widgets from styled <div> and <ul> elements — libraries like React-select or MUI Autocomplete render something that looks identical to a native select but has no underlying <select> tag. selectOption() simply does not work on these; you must interact with them exactly as a real user would, clicking the trigger element to open the panel, waiting for the option list to render, and then clicking the specific option by its role and text.
Cricket analogy: selectOption() works directly on a native <select>, like a scorer flipping a physical toggle switch on the scoreboard to change the format shown — but a custom div-based dropdown needs an actual click to open the panel and another click on the visible option, like a manual scoreboard operator physically swapping panel cards.
// Native <select> - direct, no UI simulation needed
await page.getByLabel('Country').selectOption('IN');
await page.getByLabel('Country').selectOption({ label: 'India' });
// Custom div-based dropdown (e.g. React-select) - must click through
await page.getByRole('combobox', { name: 'Country' }).click();
await page.getByRole('option', { name: 'India' }).click();Working with iframes
page.frameLocator('iframe.payment-widget').getByLabel('Card number') scopes every subsequent locator to inside that iframe's own separate document. This is necessary because a plain page.locator() operates only on the main page's DOM and has no visibility into an iframe's content at all — the two documents are entirely isolated from each other in the browser's own model, not just in Playwright's API. When an iframe contains another nested iframe, you chain frameLocator() calls, each one stepping one level deeper: page.frameLocator('#outer').frameLocator('#inner').getByRole(...).
Cricket analogy: page.frameLocator() is like a commentary team switching to a separate broadcast feed from the boundary-rope camera crew, which operates its own independent production booth — a regular page.locator() can't see inside that separate feed at all, and nested iframes require chaining frameLocator calls.
Payment widgets like Stripe Elements are deliberately rendered inside an iframe for PCI-DSS compliance, isolating raw card data from the host page's JavaScript. Testing these flows always requires page.frameLocator('iframe[name^="__privateStripeFrame"]') or similar rather than a plain page.locator().
Handling New Tabs and Popup Windows
A link with target="_blank" opens content in a brand-new browser tab, which Playwright represents as a completely separate Page object that your test must explicitly capture — it does not automatically become part of the original page reference. The standard pattern wraps the triggering click in a Promise.all alongside page.waitForEvent('popup'), so Playwright captures the new Page the instant it's created, after which you can interact with it exactly like any other page: newPage.getByRole(...), newPage.locator(...), and so on.
Cricket analogy: When a target=_blank link opens a new tab, it's like a broadcaster suddenly cutting to a second, independent camera feed at the boundary that the control room must explicitly patch in — Playwright needs page.waitForEvent('popup') wrapped around the triggering click to explicitly capture that new Page object.
Handling Native Browser Dialogs
Native window.alert(), confirm(), and prompt() dialogs block the browser's entire JavaScript event loop until they're answered, and Playwright cannot click a native OS-rendered dialog directly. Instead, you register a handler before triggering the action that opens the dialog: page.on('dialog', dialog => dialog.accept()) (or dialog.dismiss(), or dialog.accept('some text') for a prompt). If no handler is registered, Playwright's default behavior automatically dismisses the dialog, which may not match what your test actually expects, leading to unexpected downstream state or, in some cases, a hung test.
Cricket analogy: A native confirm() dialog asking 'Are you sure you want to retire the batter?' blocks the entire browser event loop until answered, like a match halting completely until the on-field umpire's decision is confirmed — you must register page.on('dialog', d => d.accept()) before triggering the action, or the test hangs.
If you don't register a page.on('dialog', ...) handler before triggering the action that opens it, Playwright's default behavior is to auto-dismiss alert/confirm/prompt dialogs. This often silently diverges from what your test expects — a confirm() the app expected to be accepted gets dismissed instead — leading to confusing state mismatches or, if the app itself waits synchronously, a hung test.
- selectOption() only works on native <select> elements; custom div-based dropdowns need click-to-open then click-option interaction.
- page.frameLocator() scopes locators inside an iframe's separate document, since page.locator() cannot pierce into it.
- Nested iframes require chaining multiple frameLocator() calls, one per level.
- Payment widgets like Stripe Elements are commonly rendered in iframes for PCI compliance.
- target=_blank links open a new, separate Page object that must be explicitly captured via page.waitForEvent('popup').
- Native alert/confirm/prompt dialogs block the entire browser event loop and must be handled via page.on('dialog', ...).
- Failing to register a dialog handler before the triggering action risks a hung test or an unexpected default dismissal.
Practice what you learned
1. Why doesn't selectOption() work on a custom dropdown built with React-select?
2. Why is page.frameLocator() necessary to interact with content inside an iframe?
3. What must you do to interact with a new tab opened by a target=_blank link?
4. What happens if you don't register a page.on('dialog', ...) handler before an action that opens a native confirm() dialog?
Was this page helpful?
You May Also Like
Locators: getByRole, getByText, and CSS
Learn how Playwright locators find elements using role-based, text-based, and CSS selector strategies, and why role-based locators are the recommended default.
Interacting with Forms and Inputs
Master Playwright's methods for filling text inputs, selecting options, checking boxes, and uploading files in real-world forms.
Auto-Waiting and Actionability Checks
Understand how Playwright automatically waits for elements to become actionable before interacting with them, eliminating most manual sleeps and flaky waits.