How to Test Embedded Widgets and iFrames
Short answer
Stripe Checkout, captcha, analytics, and partner widgets load in cross-origin iframes asynchronously—tests that interact before the frame is ready flake with "element not found". Use Playwright page.frameLocator(), wait for inner locators visible, handle nested frames (3DS, Stripe Elements), test SDK slow-load with stubs optional, and probe Assert on payment/session outcome—not parent page alone.
Part of Testing Guides by integrations.
Who this is for
Apps embedding Stripe Elements, PayPal, reCAPTCHA, Intercom, Calendly, maps, or partner white-label widgets in iframes—especially checkout and auth flows.
Why testing embeds matters
- Revenue loss — checkout iframe never loads; user cannot pay
- Intermittent CI — test clicks parent DOM while iframe still loading
- Security — clickjacking overlays; CSP blocking embed in prod only
- 3DS / SCA — nested iframe challenge untested
Complexity map
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| SDK load race | Interact too early | Zero frames | Wait inner input visible |
| Cross-origin | Cannot use page.locator | Timeout | frameLocator |
| Nested iframe | 3DS challenge | Wrong frame | Chain frameLocators |
| Multiple embeds | Wrong widget | Click misfire | Name/title filter |
| CSP blocking | Works local not prod | Missing test | Assert iframe src loads |
| Lazy embed | Scroll into view | Not attached | scrollIntoViewIfNeeded |
| Stripe Elements | Fields in iframe | fill on parent fails | frameLocator + getByLabel |
| Cookie third-party | Blocked in Safari | Session fail | Document browser matrix |
| Widget destroy | SPA route change | Orphan iframe | Assert cleanup on navigate |
| Test keys | Live key in CI | Real charges | Test publishable keys only |
frameLocator basics
From Playwright frames docs:
// Stripe Payment Element pattern (illustrative selectors)
const stripeFrame = page.frameLocator('iframe[name^="__privateStripeFrame"]').first();
await stripeFrame.getByRole('textbox', { name: /card number/i }).fill('4242424242424242');
await stripeFrame.getByRole('textbox', { name: /expiration/i }).fill('12/34');
await stripeFrame.getByRole('textbox', { name: /cvc/i }).fill('123');
frameLocator pierces cross-origin for interaction—unlike accessing contentFrame() in raw JS.
Wait for embed readiness
await page.goto('/checkout');
const paymentFrame = page.frameLocator('[data-testid="payment-iframe"]');
await paymentFrame.getByRole('textbox', { name: /card/i }).waitFor({ state: 'visible', timeout: 30_000 });
Never interact with parent "Pay" until inner fields accept input.
Nested frames (3DS)
const outer = page.frameLocator('iframe#stripe-challenge');
const inner = outer.frameLocator('iframe#acs-challenge');
await inner.getByRole('button', { name: /complete/i }).click();
Stripe 3DS test cards trigger predictable challenge flows—see Stripe testing docs.
Multiple embeds on page
const analytics = page.frameLocator('iframe[title="Analytics"]');
const chat = page.frameLocator('iframe[title="Support chat"]');
// Scope locators per frame—never page.getByRole globally
Slow SDK stub (optional)
For non-payment widgets, stub slow script load:
await page.route('**/partner-sdk.js', async route => {
await new Promise(r => setTimeout(r, 2000));
await route.continue();
});
await expect(page.getByText(/loading widget/i)).toBeVisible();
await paymentFrame.getByRole('button').waitFor();
Probe outcome after embed interaction
await page.getByRole('button', { name: /pay/i }).click();
await expect.poll(() => probeOrderStatus(orderId)).toBe('paid');
See Stripe webhooks when payment completes via webhook after iframe.
Captcha embeds
reCAPTCHA/hCaptcha often block automation—use:
- Test bypass key in staging (captcha guide)
- Probe session after server-side test flag
- Do not scrape captcha images in CI
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| page.locator inside iframe src | Cross-origin block | frameLocator |
| No wait for inner field | Race flake | waitFor visible |
| Live payment keys in CI | Real charges | Test mode keys |
| Assert parent URL only | Payment failed inside frame | Probe order status |
| Single global timeout 5s | Slow 3G embed | 30s on inner wait |
| Ignore nested frame | 3DS stuck | Chain locators |
Example scenario
Situation: User completes Stripe Payment Element checkout in embedded iframe.
Expected outcome: Payment succeeds in test mode; probe order paid after webhook or sync confirm.
Why UI-only automation breaks: Parent page shows success redirect but iframe payment declined—probe still pending.
- Arrange: Seed checkout session orderId; Stripe test keys; webhook or sync probe ready.
- Act: Fill card fields inside frameLocator; submit pay on parent or inner.
- Assert: Probe order paid; optional frame error message absent.
TestChimp workflow: Track embed_provider when new partner iframe ships without interaction test.
Same Arrange/Act/Assert pattern as expired-coupon checkout.
Connect scenarios to your QA workflow
Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).
Related scenarios
- Stripe payments — test cards and Checkout
- Stripe webhooks — post-payment state
- Captcha flows — iframe captcha bypass
- Google Maps — map iframes
External references
Frequently asked questions
How do I interact with cross-origin iframes in Playwright?
Use page.frameLocator(selector)—Playwright pierces cross-origin for interaction. Chain frameLocators for nested frames like 3DS challenges.
Widget not found intermittently?
Wait for inner locator visible inside frameLocator before fill/click. Increase timeout for SDK load; optional route delay test for loading UI.
Stripe fields not filling?
Card inputs live inside Stripe iframe—use frameLocator with role/label locators, not parent page getByLabel.
How do I test 3DS nested iframes?
Use test cards triggering challenge; chain outer.frameLocator().frameLocator() to reach Complete Authentication button.
Can I test captcha iframes in CI?
Use provider test keys or server bypass flag in test env—probe auth success. Do not automate image captcha solving in PR CI.
Multiple iframes on page—which frame?
Filter by title, name, or data-testid on iframe element. Scope all locators from specific frameLocator—avoid page-level locators.
New partner embed without tests?
TrueCoverage embed_provider slice flags gap. Add frameLocator SmartTest via /testchimp evolve when partner widget goes live.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.