Controls Inside the Frame
When a control lives inside an iframe, point the locator at that frame with frameLocator. A locator on page searches the outer document and misses it.
This post is part of the Playwright Essentials series. If you missed the previous post, read Part 15 — Assert on the Page the Click Opened first.
Introduction — The Input Is on the Screen
The test sees the card number field on the checkout screen. page.getByLabel('Card number') waits until the timeout and fails. The input is inside an iframe from the payment provider, and page only searches the outer document.
Part 15 switched to the popup because the click opened another window. An iframe stays in the same tab, but it owns a separate document. Your locator has to cross that boundary before it looks for the control.
Enter the Frame with frameLocator
page.frameLocator() points the next locator at the document inside the iframe. Like any other locator, it waits for the frame to appear and survives a reload of that frame.
test('fills the card number inside the payment frame', async ({ page }) => {
await page.goto('/checkout');
const paymentFrame = page.frameLocator('iframe[title="Secure payment"]'); // the document that holds the card inputs
await paymentFrame.getByLabel('Card number').fill('4242424242424242');
await page.getByRole('button', { name: 'Pay 49.00' }).click();
await expect(page.getByRole('heading', { name: 'Payment received' })).toBeVisible();
});
The decision is frameLocator. Only the card input lives inside the frame. The Pay button and the confirmation heading belong to checkout, so those calls stay on page.
Inside the frame, keep the locator order from part 2: role, label, then test ID. Crossing into an iframe changes the document you search, not how you choose selectors.
Prefer frameLocator Over page.frame()
page.frame({ name: 'payment' }) returns the frame as it exists at that moment, or null if the tag has not mounted yet. If the payment widget reloads its iframe after an address change, that reference points to a detached frame.
frameLocator resolves the frame on every action. Use frameLocator whenever you click, fill, or assert on a control. Reach for page.frame() only when you need a frame method that a locator does not expose, such as reading the frame URL.
Do Not Assert on the iframe Tag Alone
Checking that page.locator('iframe[title="Secure payment"]') is visible only proves that the outer page rendered a box. The document inside that box can still be blank, blocked by a header, or stuck on an error screen.
Assert on a control or a message inside paymentFrame. When a provider hosts one input per frame — one for the card number, one for the expiry date, one for the security code — give each iframe its own frameLocator named after its title. Avoid .first() on iframe tags; a third-party analytics frame earlier in the DOM will take that slot.
Conclusion
When a form or widget renders inside an iframe, start the locator chain with page.frameLocator() and use normal role or label locators inside it. Leave page for the controls that belong to the outer document, and never treat a visible iframe tag as proof that the embedded form loaded.
This week’s exercise: Find one test that checks an embedded payment form, chat widget, or captcha only by asserting on the outer page, or that works around an iframe by skipping the step. Point frameLocator at that iframe by its title attribute and assert on a control inside the frame.