How to Handle Iframes in Cypress
Test same-origin iframe content in Cypress with retryable commands, reusable helpers, and clear guidance for cross-origin limits and workarounds.
Direct answer: For a same-origin iframe, query its contentDocument.body, wait until the body is non-empty, then wrap it with cy.wrap(). Cypress retries the query and assertion while the frame loads, and the wrapped body supports normal Cypress queries and actions. Cypress cannot automate or communicate with a cross-origin iframe under normal browser security rules. cy.origin() handles top-level navigation to another origin; it does not switch into an embedded frame.
1. Check whether the iframe is same-origin
Two documents have the same origin when their scheme, host, and port match. A frame served from the same origin as the page can generally be inspected through the DOM. A frame from another origin is subject to the browser’s same-origin policy, so Cypress cannot read its document in the normal setup.
Payment forms, video players, identity-provider login forms, and comment widgets are often hosted on another origin. Check the actual iframe src and redirects rather than assuming based on the vendor name. The browser’s security boundary is the key constraint, not the iframe’s visual appearance.
2. Query and interact with a same-origin iframe
Use a stable selector for the frame, especially when the page has more than one. This example waits for the iframe body, finds a submit button, and clicks it:
cy.get('iframe[data-testid="checkout-frame"]')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
.find('[data-testid="submit"]')
.click()
The .its() query and non-empty assertion allow Cypress to retry while the document becomes available. cy.wrap() turns the body into a Cypress subject, so chained commands such as .find(), .should(), and .click() keep Cypress’s normal retry behavior.
Assertions and actions work the same way once the body is wrapped:
cy.get('iframe[data-testid="profile-frame"]')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
.find('[name="email"]')
.should('have.value', 'person@example.com')
cy.get('iframe[data-testid="profile-frame"]')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
.find('[name="email"]')
.clear()
.type('new@example.com')
3. Make a reusable iframe helper
If several tests query same-origin frames, a small custom command avoids repeating the traversal. Add this to your Cypress support file, such as cypress/support/commands.js:
Cypress.Commands.add('getIframeBody', (selector) => {
return cy
.get(selector)
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
})
Then use it in a spec:
cy.getIframeBody('iframe[data-testid="checkout-frame"]')
.find('[data-testid="submit"]')
.click()
This helper is for same-origin frames whose document Cypress can access. It assumes the selector matches the intended iframe; if it can match multiple frames, make it more specific.
4. Choose between native commands and a plugin
| Approach | Use it when | Notes |
|---|---|---|
Native contentDocument.body traversal |
You need to inspect a same-origin frame in one or a few tests. | No dependency required; the retry-aware pattern is short. |
| Custom Cypress command | Your suite repeatedly accesses same-origin frames. | Keeps the selector and body-wait pattern consistent across specs. |
cypress-iframe community plugin |
You want packaged convenience helpers such as cy.iframe() and cy.frameLoaded(). |
Optional convenience, not a built-in Cypress command or a prerequisite for modern same-origin iframe tests. |
Follow the plugin’s installation and version instructions in its project documentation if you choose it. The Cypress migration guide also demonstrates a custom iframe-body helper.
5. Understand cross-origin iframe limits
When an iframe is cross-origin, its contentDocument is inaccessible to the parent page; Cypress documents that reading it returns null. Cypress says it cannot automate or communicate with a cross-origin iframe embedded in the page. The same-origin policy is enforced by the browser.
For an integrated flow, test what your application controls: for example, assert that the parent page mounts the provider iframe, displays the expected surrounding state, and handles the documented success or failure callback. Where the application permits it, use a test seam owned by your application to exercise the integration without claiming Cypress interacted with the third-party frame. A provider’s own test environment or a separately supported test strategy may be needed for the provider UI itself.
6. Know what cy.origin() does
cy.origin() scopes Cypress commands to a secondary origin after the top-level page navigates there, such as after a link, form submission, or redirect. It does not enter an embedded iframe. Cypress explicitly lists commands inside an iframe among the scenarios that cy.origin() cannot handle.
There is also a version detail for top-level navigation: starting with Cypress v14, Cypress no longer injects document.domain by default. Tests that move between origins in one test must use cy.origin(), including some related-subdomain transitions that older behavior handled differently. The deprecated injectDocumentDomain setting is not a way to enable iframe automation and can cause issues such as problems with origin-keyed agent clusters.
7. Treat disabling web security as a narrow workaround
The Cypress FAQ describes chromeWebSecurity: false as a possible workaround for cross-origin iframe access in Chromium-family browsers. It is not supported in Firefox or WebKit, and it changes browser security behavior. Do not treat it as general cross-origin iframe support or assume a test is representative of normal user security conditions.
If you consider this setting, scope it to the specific Chromium test configuration and validate that the workflow actually works in that setup. Keep assertions focused on your application-owned integration behavior. The setting does not make cy.origin() an iframe switch.
8. Separate iframe testing from CSP testing
Cypress documents a separate Content Security Policy limitation: the frame-ancestors directive prevents Cypress from loading a test application into an iframe. Cypress also strips certain directives unconditionally, so their behavior cannot be tested using Cypress. This concerns loading the test application inside a frame and is distinct from querying an iframe embedded by the application.
9. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
contentDocument is null |
The iframe is cross-origin, or its document is not accessible yet. | Confirm the frame origin. For same-origin content, wait for a non-empty body as shown above. For cross-origin content, use an application-owned test seam or test the parent integration. |
| Find or click runs before frame content appears | The iframe loads asynchronously. | Assert that contentDocument.body is not empty before wrapping and querying it. |
| The wrong iframe is queried | A broad selector matched a different iframe, or the page contains several. | Use a stable, specific selector tied to the relevant frame. |
| Commands work on the body but not the expected nested element | The selector is absent, the frame content differs from the assumption, or the content has not finished rendering. | Check the rendered same-origin frame DOM and assert for a stable test attribute after the body becomes non-empty. |
cy.origin() does not reach the embedded provider |
cy.origin() applies to top-level origin navigation, not embedded frames. |
Use it only after the top-level page changes origin; handle embedded cross-origin integrations at the parent or through a test seam. |
chromeWebSecurity: false has no effect in the selected browser |
The documented workaround is limited to Chromium-family browsers. | Use a supported Chromium configuration if you need to evaluate that narrow workaround, or avoid direct frame access. |
| Cypress cannot load the app in a test iframe | The app’s CSP uses frame-ancestors. |
Recognize this as the separate CSP limitation; Cypress documents that this directive cannot be tested with Cypress. |
10. Reliability, performance, and cost considerations
For same-origin frames, waiting on the actual body state is more reliable than adding a fixed delay: it ties progress to the condition the test needs. Keep the frame selector stable and avoid querying before the frame exists. If the iframe is replaced during app rendering, reacquire it through the Cypress chain rather than retaining a stale DOM object.
There is no useful universal timing number for iframe loading: it depends on the application and frame content. A fixed wait can make a test slower when the frame loads quickly and flaky when it loads more slowly. Retryable queries and assertions express the required condition directly. The research sources provide no relevant cost figures; practical cost depends on your Cypress execution environment and test suite.
11. Capture a page while investigating an iframe
If you need a visual record of the parent page or an embedded area while debugging, ScreenshotNeo is a website screenshot API and MCP server for developers. A screenshot can document what the page presents, but it does not grant Cypress access to a cross-origin frame or replace interaction tests.
Or skip the browser setup
For a screenshot, one GET request returns an image or PDF. This cURL example saves a WebP screenshot of a page that embeds an iframe:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
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)
Node.js:
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 Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));
The API also accepts parameters used by other screenshot APIs. You can configure full-page or element capture, viewport and device presets, wait conditions, custom CSS or JavaScript, headers, cookies, and other capture options. See the ScreenshotNeo API documentation for parameter names and usage. Visit ScreenshotNeo for product details.
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed; response headers report the page verdict and billing status. Its MCP server lets AI agents use screenshot, page-info, and PDF tools. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.
12. FAQ
Does Cypress have a built-in switch-to-iframe command?
No dedicated switch command is needed for same-origin frames; query the frame body and chain Cypress commands from the wrapped subject.
Can Cypress test a YouTube or Stripe iframe directly?
Not under normal cross-origin browser security. Test the parent application’s integration behavior, or use a test strategy supported by the embedded provider.
Does the Cypress iframe plugin remove the origin restriction?
No. A helper plugin makes frame-body access more convenient; it does not override the browser’s same-origin policy.
Can a screenshot prove the iframe works?
No. A screenshot records rendered appearance. It does not verify interaction or grant access to a cross-origin frame’s document.


