Why does cy.origin() not help a Cypress test reach into a cross-origin iframe?
answer
- Top-level navigation, not embedded frames
- contentDocument reads null across origins
- Same-origin frames are queryable today
- its, then cy.wrap, then chain
- No switch-into-frame command exists
basics
~20 scy.origin() handles top-level navigation to another origin, not embedded frames. A cross-origin iframe's contentDocument reads as null under the same-origin policy, so no Cypress command can get a handle on its DOM. Same-origin iframes can be queried.
solid answer
~40 s`cy.origin()` works by injecting a second Cypress instance into the origin the **top document** navigated to. An embedded `<iframe>` from another origin is a different situation: the browser's same-origin policy makes its `contentDocument` read as `null` from the parent, so there is no document for any Cypress command to attach to, and wrapping the query in `cy.origin()` changes nothing. Cypress does not support cross-origin iframes; a payment form, an embedded video or a third-party PDF viewer is out of reach. **Same-origin** iframes are fine — grab the frame, read `.its('0.contentDocument.body')`, assert it is not empty, pass it through `cy.wrap()` to restore retry-ability, then chain as usual. For a vendor frame, test your own side of the contract instead: the frame's attributes, and how your app behaves when the vendor's request is stubbed or fails.
code
javascript · 10 linesit('reads the page count from the same-origin inline viewer', () => {
cy.visit('https://statements.example.com/statements/2026-08')
cy.get('iframe[data-cy=inline-viewer]')
.its('0.contentDocument.body') // raw DOM node; .its() retries
.should('not.be.empty') // the frame document loads async
.then(cy.wrap) // back to a retryable subject
.find('[data-cy=page-count]')
.should('have.text', '4')
})go deeper
Know that Cypress has no switch-into-frame command, and that an iframe from another origin is off limits while a same-origin one can be queried.
Explain the difference between top-level navigation, which cy.origin() solves, and an embedded cross-origin document, which the same-origin policy keeps unreadable.
Expect to diagnose a timeout whose real cause is a vendor iframe, and to propose what the suite asserts instead: the frame's attributes, the stubbed failure path, or a same-origin stand-in.
Be ready to decide how much third-party embedded surface a suite should chase at all, and where that coverage belongs if not in the browser end-to-end layer.
## Two different problems that look like one Both cases involve content from another origin, but the mechanisms are unrelated. **Top-level navigation.** Your page goes to another origin — a redirect, a link, a form post. Cypress can solve this because it controls the frame the application lives in: it injects a copy of itself into the new origin, talks to it over a bidirectional channel, and evaluates your callback there. That is `cy.origin()`. **An embedded cross-origin frame.** Your page stays where it is and renders a `<iframe>` pointing at someone else's origin. Now the browser is enforcing the boundary *between two live documents in one page*. Reading `contentDocument` or `contentWindow` from the parent gives `null`, and Cypress has no privileged position from which to do otherwise — it is page script, subject to the same rule. There is nothing for `cy.origin()` to inject itself into, because the frame is not where the test is. Common examples in a bank statement app: an embedded third-party PDF viewer, a card-capture form from a payments vendor, an identity widget rendered inline rather than by redirect. ## What works today, and what does not | Situation | Supported? | Approach | | --- | --- | --- | | Top-level navigation to another origin | Yes | wrap commands in `cy.origin()` | | `<iframe>` served from the same origin | Yes | query into `contentDocument.body` | | `<iframe>` served from another origin | No | assert the contract, not the contents | | Another browser tab or window | No | out of scope for the runner entirely | A Chromium-only escape hatch exists in the form of the `chromeWebSecurity` configuration option, which lets those browsers reach into cross-origin frames; it is a trade-off with its own consequences and it does not apply to Firefox or WebKit. ## Querying into a same-origin frame There is no switch-into-frame command in Cypress — the DOM is reached with ordinary queries instead: ```js cy.get('iframe[data-cy=inline-viewer]') .its('0.contentDocument.body') .should('not.be.empty') .then(cy.wrap) .find('[data-cy=page-count]') .should('have.text', '4') ``` Each step earns its place: - `.its('0.contentDocument.body')` indexes into the jQuery object to get the raw `<iframe>` element and reads its body; `.its()` retries, so it waits for the frame to exist - `.should('not.be.empty')` waits for the frame's document to render, because an `<iframe>` loads asynchronously and an empty body is the classic flake here - `cy.wrap()` turns the raw DOM node back into a Cypress subject, which restores retry-ability for everything chained after it - wrapping the three lines in a custom command keeps specs readable when several tests need it ## Testing a vendor frame you cannot enter Losing access to the vendor's DOM is less damaging than it first looks, because the vendor's rendering is their test surface, not yours. What is genuinely yours: 1. **The handoff.** Assert that the `<iframe>` renders and that its `src` carries the right document identifier, token or locale — that is the contract your application owns. 2. **The failure paths.** Stub the document request with `cy.intercept()` and assert your app's own fallback: an error banner, a retry control, a download link. 3. **The layout.** Sizing, focus order and the surrounding page state are all in your document. 4. **A same-origin stand-in.** If the interaction inside the frame is genuinely worth covering, serve a minimal same-origin viewer in test builds and drive that; you keep the integration honest at the boundary and get a real DOM to assert on. ## Diagnosing it in a real suite Confirm the diagnosis in one step rather than guessing at it: read the frame's `src` in the Command Log snapshot, or evaluate `document.querySelector('iframe').contentDocument` in the browser console while the run is paused. A `null` there settles the question before anyone starts editing selectors. The symptom is a `cy.get()` that times out on an element a human can plainly see, usually with a `null` where a document was expected further up the chain. The tell is the element's ancestry: if it lives inside an `<iframe>` whose `src` points somewhere other than the application's own origin, no selector, timeout or plugin will reach it. Community helpers exist for iframes — the `cypress-iframe` npm package packages the same-origin pattern above — but none of them can defeat the browser's same-origin policy, and treating one as a fix wastes a debugging session.
- Why is cy.wrap() needed after .its('0.contentDocument.body') in a Cypress iframe query?`.its()` yields a **raw DOM node**, not a Cypress subject, so the chain loses retry-ability and the DOM helpers that follow it. `cy.wrap()` turns the node back into a subject, so `.find()`, actions and assertions after it retry normally. The `.should('not.be.empty')` before it matters just as much: an iframe document renders asynchronously.
- What can a Cypress test still assert about a third-party viewer it cannot enter?Everything on your side of the boundary: that the `<iframe>` is rendered and sized, that its `src` carries the correct document id and token, and how your application behaves when the vendor's request is stubbed to fail — error banner, retry, fallback download. That covers the integration you own without depending on someone else's DOM.
saying these in an interview costs you the question
- Wraps an iframe query in cy.origin() and expects it to work
- Blames the selector when contentDocument is null
- Believes a plugin can defeat the browser's same-origin policy
- Drives the vendor's own UI instead of the app's contract with it
- Chains .find() straight off .its() without cy.wrap()