ScreenshotNeo

BlogHow-to

How to Work with Iframes in Cypress

Learn when Cypress can access an iframe, how to query a same-origin frame, and what to do when the embedded frame is cross-origin.

By the ScreenshotNeo team4 October 20267 min read

Short answer: Cypress can query elements inside a same-origin iframe by reading its contentDocument.body, waiting for the body to load, and wrapping it with Cypress. Cypress cannot automate or communicate with a cross-origin iframe embedded in your page. cy.origin() supports commands after navigating to a different page origin; it does not let Cypress enter a cross-origin iframe.

1. Check whether the iframe is same-origin

An origin is the combination of a URL’s scheme, hostname, and port. The iframe and parent must match on all three for the parent page to access the frame document under the browser’s same-origin policy. For example, https://app.example.test and https://payments.example.test are different origins, as are http://app.example.test and https://app.example.test.

Inspect the iframe’s src in the application and compare its origin with the page under test. A relative URL or a URL on the same scheme, host, and port is typically same-origin. Do not infer that a frame is accessible merely because the iframe element appears in the parent DOM.

2. Query and interact with a same-origin iframe

Wait for the frame document body to become non-empty, wrap it, and then use normal Cypress queries against the wrapped body. This example assumes the iframe contains an element with data-cy="save":

cy.get('iframe')
  .its('0.contentDocument.body')
  .should('not.be.empty')
  .then(cy.wrap)
  .find('[data-cy="save"]')
  .click()

The cy.get('iframe') query finds the iframe element in the parent document. its('0.contentDocument.body') accesses the first frame’s body. The assertion retries while the body is empty, and cy.wrap gives Cypress a subject on which it can run DOM commands such as find, should, and click.

For an assertion instead of a click, use the same traversal and chain an assertion:

cy.get('iframe')
  .its('0.contentDocument.body')
  .should('not.be.empty')
  .then(cy.wrap)
  .find('[data-cy="status"]')
  .should('have.text', 'Saved')

Adapt the selector and readiness condition to your app. If the frame renders asynchronously, wait for a meaningful element or state inside it rather than adding an arbitrary delay.

Target a particular iframe

If the page contains multiple frames, scope the query to a stable parent selector or iframe attribute. For example:

cy.get('[data-cy="editor-frame"] iframe')
  .its('0.contentDocument.body')
  .should('not.be.empty')
  .then(cy.wrap)
  .find('[data-cy="editor"]')
  .should('be.visible')

Use a selector that identifies the intended frame; selecting the first iframe on a page can silently target the wrong one when analytics, ads, or other embedded content is present.

Put the traversal in a custom command

When several tests need the same same-origin traversal, a custom command can keep the access pattern consistent. Add this to your Cypress support file, such as cypress/support/commands.js:

Cypress.Commands.add('getIframeBody', (selector = 'iframe') => {
  return cy.get(selector)
    .its('0.contentDocument.body')
    .should('not.be.empty')
    .then(cy.wrap)
})

Then use it in a test:

cy.getIframeBody('[data-cy="editor-frame"] iframe')
  .find('[data-cy="save"]')
  .click()

This helper is for same-origin frames. It does not bypass the browser security boundary for cross-origin content. If an application has multiple independently loading frames, make the helper’s selector and readiness condition explicit for the frame being tested.

3. Understand the cross-origin limitation

Cypress documents that it cannot automate or communicate with a cross-origin iframe embedded in the application. Common examples include third-party video players, payment forms, login widgets, and comment embeds. The browser’s same-origin policy prevents the parent page from reading the frame document, so the same contentDocument.body approach cannot reach its contents. Cypress’s cross-origin testing guide describes this limitation.

cy.origin() does not change that. It is for running Cypress commands after a test navigates to a secondary page origin. Cypress’s API documentation explicitly lists commands inside an iframe as unsupported by cy.origin(). See the cy.origin() API reference.

As of Cypress 14.0.0, Cypress no longer injects document.domain by default. Tests that navigate between different origins, including origins on the same superdomain, may therefore need cy.origin(). This is about page navigation between origins; it does not make an embedded cross-origin iframe automatable.

Choose a test boundary that matches the integration

  • You own the embedded app: visit its URL directly in a separate test and verify its behavior there. This tests the app’s own page, not control of it while embedded.
  • You need to verify the parent integration: assert observable behavior in the parent, such as the iframe being present, its configured URL, or the parent application’s response to a completed integration flow.
  • The integration has a supported service boundary: test through that application or service boundary where appropriate, and keep the parent-page test focused on the behavior your application owns.
  • You need confidence in a third-party frame’s internal UI: use the provider’s own test or integration facilities if available. Cypress’s documented iframe limitation is not a selector or timing problem.

These are test-design choices based on Cypress’s documented restriction, not Cypress-provided ways to control a cross-origin embedded frame.

4. Common errors and fixes

Symptom Likely cause What to do
contentDocument is null, inaccessible, or errors when read The frame is cross-origin, or its document is not ready. Compare scheme, hostname, and port. For a same-origin frame, wait for its body or a meaningful child element. For a cross-origin frame, choose a parent-level or separate-page test boundary.
The body is empty or a query finds no elements The iframe exists before its content has rendered, or the selector does not match the frame DOM. Wait for a stable element inside the same-origin frame, verify the selector against the frame’s rendered DOM, and account for app-specific loading.
The test queries the wrong frame A broad iframe selector matched a different frame first. Scope the query to a stable parent or iframe attribute and target the intended frame explicitly.
cy.origin() still cannot access the embedded content cy.origin() was used as an iframe switch. Use it only for commands after page navigation to another origin. It does not enter an iframe.
Disabling chromeWebSecurity does not make the test portable Browser security behavior and Cypress support vary; this is not a general cross-origin iframe solution. Do not treat the setting as a promise of cross-origin iframe automation. Design the test around an observable parent behavior or test the embedded application independently.
The test passes locally but flakes in CI The frame is ready at different times, or the test relies on a fixed delay or unstable selector. Prefer Cypress retryable queries and assertions tied to an app state. Use stable test attributes and verify the same browser and origin configuration used in CI.

5. Reliability and performance notes

  • Wait on state, not elapsed time. Cypress retries linked queries and assertions. A body-not-empty check or a meaningful in-frame condition is usually more resilient than a hard-coded sleep.
  • Keep selectors stable. Test attributes are less likely to change with styling than deeply nested CSS selectors.
  • Scope frame access. A page can have multiple iframes, and their load order can vary. Identify the frame you intend to test.
  • Keep ownership clear. A parent-page test should verify what the parent owns. A third-party provider’s internal controls are outside Cypress’s supported cross-origin iframe automation.
  • Use browser security settings cautiously. Cypress discusses chromeWebSecurity in some Chromium-family cases, but current cross-origin guidance still says cross-origin iframes are unsupported. Validate any setting against the exact browser and environment; do not build a test strategy around bypassing the documented boundary.

Same-origin frame traversal adds ordinary DOM queries; the important reliability cost is waiting for the frame and choosing a stable boundary. Repeated fixed delays make suites slower while still failing to guarantee readiness.

6. Or skip the browser setup

If you need a visual screenshot of a page that contains an iframe, ScreenshotNeo can capture the page through its screenshot API. A screenshot is useful for visual review; it does not interact with or validate controls inside a cross-origin iframe.

One GET request returns an image or PDF. See the ScreenshotNeo API documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers say which page verdict and billing outcome applied. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up for 1,000 free screenshots a month, no card required.

7. FAQ

Can Cypress click a button inside a same-origin iframe?

Yes. Read and wrap the iframe body, wait for it to load, then find and click the button as shown above.

Does cy.origin() support iframe interaction?

No. It supports commands after navigation to a different page origin, not commands inside an embedded iframe.

Does this work with every iframe?

No. The direct DOM method requires same-origin access. Cross-origin embedded frames remain outside Cypress’s supported automation.

Can I use a screenshot to verify an iframe interaction?

A screenshot can show the rendered page for visual inspection, but it does not prove that a control inside the frame can be automated or that the interaction succeeded.

References