Skip to content

The Same-Origin Constraint, Investigated

What was run

The exact same contentDocument workaround this course verified against a same-origin iframe in Level 2, Lesson 5, run again against a genuinely cross-origin iframe — a real second Express server on localhost:5152, embedded in a page served from localhost:5151 — to directly confirm where that workaround's real boundary actually sits.

it("fails to reach a real cross-origin iframe's contentDocument", () => {
  cy.visit("/cross-origin-iframe.html"); // iframe src="http://localhost:5152/cross-origin-frame-content"
  cy.get("#cross-frame")
    .its("0.contentDocument.body.innerHTML")
    .should("not.be.empty");
});

it("confirms the same-origin workaround from Level 2 works for a same-origin iframe", () => {
  cy.visit("/iframe.html"); // iframe src is same-origin, localhost:5151
  cy.get("#content-frame")
    .its("0.contentDocument.body.innerHTML")
    .should("not.be.empty");
});
1) fails to reach a real cross-origin iframe's contentDocument:
   CypressError: Timed out retrying after 4000ms: `cy.its()` errored because the property:
   `0.contentDocument.body.innerHTML` does not exist on your subject.

✓ confirms the same-origin workaround from Level 2 works for a same-origin iframe (41ms)

The identical code, the identical Cypress command, against two real iframes differing in exactly one way (same-origin vs. a genuinely different real server on a different port) — and the real result is completely different. The cross-origin iframe's contentDocument genuinely doesn't exist as an accessible property at all from the host page's perspective.

How It Actually Works

This isn't a Cypress limitation — it's the real browser's own, standard same-origin policy, which blocks any script (Cypress's own in-browser test code included, per Lesson 1) from reading a cross-origin iframe's contentDocument property at all. The browser doesn't even return a restricted-but-readable object; it simply refuses to expose the property, which is exactly why Cypress's own cy.its() reports it as not existing rather than reporting a permission error. Level 2, Lesson 5's workaround only ever worked because that iframe happened to be same-origin — this lesson closes the loop that lesson left open, confirming with a genuinely different real origin exactly where that workaround's limit is.

cy.origin() (Lesson 3) doesn't help here either, despite also dealing with cross-origin boundaries — cy.origin() lets Cypress's own test commands run inside a different origin's context after a full page navigation; it has no mechanism for reading into a live cross-origin iframe embedded within the current page, because the browser's own security model draws that line at the DOM property level, not at the level of which Cypress API you call.

Common mistakes

  • Assuming any contentDocument-based workaround generalizes to all iframes — this lesson's own real, side-by-side comparison shows it depends entirely on whether the iframe is same-origin, not on anything about how carefully the workaround code is written.
  • Reaching for cy.origin() to solve a cross-origin-iframe problem — it solves a different real problem (commands after a full navigation to a different origin), not reading into an embedded iframe from the page that contains it.
  • Treating this as something a plugin can cleanly fix — cross-origin iframe content inspection is blocked by the real browser's own security model; a plugin can at best offer narrow workarounds (postMessage bridging the frame itself implements, or testing the iframe's content as its own separate page via a direct cy.visit()), not a general bypass.

Exercise

Modify cross-origin-frame-content on the real second server to postMessage its own button-click events back to the parent window, and write a real Cypress test that listens for that message via cy.window().then(win => { win.addEventListener("message", ...) }) instead of trying to reach into the iframe's DOM directly. Confirm whether this real, cooperative pattern — common for payment widgets and other legitimate cross-origin embeds — can verify the cross-origin button's behavior without ever reading its contentDocument.