Iframe steps let scenarios reach inside embedded frames - payment widgets, captchas, embedded videos, third-party forms - that normal steps cannot see because they only search the top document. The group is backed by Playwright's frameLocator API: you switch the active scope to a frame, run frame-scoped click / fill / assert steps, then switch back to the root document.
Switching is cheap and lazy. The switch step stores a frame locator on the scenario world; the frame itself is resolved when the first frame-scoped step runs, and Playwright's auto-waiting applies inside the frame just as it does outside. Frames can be addressed four ways: by CSS selector, by an explicit locator string, by the iframe's title attribute, or by its name attribute - pick whichever the embed markup makes stable.
Every frame-scoped step fails with a friendly, actionable error when no frame is active or the element is missing - typically reminding you to switch first, or to wait for the iframe to appear (When I wait for "iframe" to appear) before switching. Only the steps ending in inside the iframe honor the frame scope; all other steps keep operating on the main document.
Quick reference
| Step | What it does |
|---|---|
When I switch to iframe "iframe.payment" | Scope subsequent frame steps to an iframe by CSS selector. |
When I switch to iframe with locator "iframe.payment" | Same, phrased for explicit Playwright frame-locator strings. |
When I switch to the iframe with title "Payment" | Scope to the iframe whose title attribute matches. |
When I switch to the iframe with name "payment" | Scope to the iframe whose name attribute matches. |
When I switch to the root document | Clear the frame scope; steps target the top document again. |
When I click "Confirm" inside the iframe | Click visible text inside the active iframe. |
When I click "submit-btn" by attr inside the iframe | Click by id, class, name, or data-testid inside the active iframe. |
When I fill in "card_number" with "4242 4242 4242 4242" inside the iframe | Fill a field by [name] or id inside the active iframe. |
Then I should see "Payment received" inside the iframe | Assert visible text exists inside the active iframe. |
Then I should not see "Error" inside the iframe | Assert visible text is absent inside the active iframe. |
Steps in detail
When I switch to iframe "..." / with locator "..."
Both accept any Playwright-compatible selector for the <iframe> element - iframe.payment, #stripe-frame, iframe[name=widget], iframe[src*=embed]. The two phrasings are interchangeable; use whichever reads better. The original page handle is remembered so the scope can be restored later.
When I switch to iframe "iframe.payment"
When I switch to iframe with locator "#stripe-frame"
When I switch to the iframe with title "..." / with name "..."
Attribute shorthands that build iframe[title="..."] or iframe[name="..."] selectors for you. The title variant is the accessibility-friendly choice - well-built embeds carry a descriptive title, and it doubles as documentation in the feature file.
When I switch to the iframe with title "Stripe checkout"
When I switch to the iframe with name "captcha"
When I switch to the root document
Clears the frame scope so subsequent frame-scoped steps (and your mental model) return to the top document. Always switch back once you are done inside the frame - assertions about the host page made while a frame is active can otherwise look in the wrong place.
When I switch to iframe "#payment"
And I press "Pay"
And I switch to the root document
Then I should see "Payment received"
When I click "..." inside the iframe / by attr inside the iframe
The text variant clicks the first element whose visible text matches exactly. The by attr variant tries id, class, [name], and [data-testid] in that order - the failure message tells you all four were checked. Both require an active frame scope.
When I switch to iframe ".captcha-frame"
And I click "I am not a robot" inside the iframe
And I click "submit-btn" by attr inside the iframe
When I fill in "..." with "..." inside the iframe
Fills the first element matching [name="field"] or #field inside the active frame. This is the step for hosted payment fields, which live in cross-origin iframes precisely so the host page cannot touch them directly.
When I switch to the iframe with title "Payment"
And I fill in "card_number" with "4242 4242 4242 4242" inside the iframe
And I fill in "cvv" with "123" inside the iframe
And I fill in "exp_date" with "12/30" inside the iframe
Then I should (not) see "..." inside the iframe
Presence and absence assertions on exact visible text within the active frame - the frame-scoped counterparts of the global I should see steps.
Then I should see "Payment received" inside the iframe
And I should not see "Declined" inside the iframe
Complete example
Feature: Embedded payment widget
Scenario: Pay inside the provider's iframe and confirm on the host page
Given I am on "/checkout"
When I wait for "iframe" to appear
And I switch to the iframe with title "Stripe checkout"
And I fill in "card_number" with "4242 4242 4242 4242" inside the iframe
And I fill in "cvv" with "123" inside the iframe
And I fill in "exp_date" with "12/30" inside the iframe
And I click "Confirm" inside the iframe
Then I should see "Payment received" inside the iframe
And I should not see "Declined" inside the iframe
When I switch to the root document
Then I should see "Order complete"
Tips
- Wait for the iframe element to exist before switching (
When I wait for "iframe" to appear) - embeds that inject their frame via JavaScript often are not in the DOM on page load. - Nested frames: the scope always resolves from the top page, so target the innermost iframe with a selector specific enough to reach it (e.g.
iframe[src*=embed]). - Only
inside the iframesteps are frame-scoped. If you need richer interactions inside a frame (selects, keyboard input), drive them from JavaScript steps or extend this group.