Skip to main content

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

ScenarioEdge caseWhy tests breakApproach
SDK load raceInteract too earlyZero framesWait inner input visible
Cross-originCannot use page.locatorTimeoutframeLocator
Nested iframe3DS challengeWrong frameChain frameLocators
Multiple embedsWrong widgetClick misfireName/title filter
CSP blockingWorks local not prodMissing testAssert iframe src loads
Lazy embedScroll into viewNot attachedscrollIntoViewIfNeeded
Stripe ElementsFields in iframefill on parent failsframeLocator + getByLabel
Cookie third-partyBlocked in SafariSession failDocument browser matrix
Widget destroySPA route changeOrphan iframeAssert cleanup on navigate
Test keysLive key in CIReal chargesTest 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-patternWhy it failsBetter approach
page.locator inside iframe srcCross-origin blockframeLocator
No wait for inner fieldRace flakewaitFor visible
Live payment keys in CIReal chargesTest mode keys
Assert parent URL onlyPayment failed inside frameProbe order status
Single global timeout 5sSlow 3G embed30s on inner wait
Ignore nested frame3DS stuckChain 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.

  1. Arrange: Seed checkout session orderId; Stripe test keys; webhook or sync probe ready.
  2. Act: Fill card fields inside frameLocator; submit pay on parent or inner.
  3. 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).

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.

Start free on TestChimp · Book a demo