The most surprising thing about testing content inside iframes with Cypress is that you’re not actually testing the iframe directly; you’re testing the parent document’s interaction with the iframe’s content.

Let’s see this in action. Imagine you have a page at http://localhost:8080/parent that contains an iframe pointing to http://localhost:8080/child.

<!-- public/parent.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Parent Page</title>
</head>
<body>
    <h1>Parent Content</h1>
    <iframe id="my-iframe" src="/child.html" width="600" height="400"></iframe>
</body>
</html>
<!-- public/child.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Child Page</title>
</head>
<body>
    <h2>Iframe Content</h2>
    <p id="message">Hello from the iframe!</p>
    <button id="change-text-button">Change Text</button>
</body>
</html>

Here’s a Cypress test that interacts with this setup:

// cypress/e2e/iframe_spec.cy.js
describe('iFrame Interaction', () => {
  beforeEach(() => {
    cy.visit('/parent.html');
  });

  it('should interact with content inside an iframe', () => {
    // 1. Get the iframe element
    cy.get('#my-iframe').then(($iframe) => {
      // 2. Access the iframe's document
      const iframeDocument = $iframe.contents().find('body');

      // 3. Interact with elements within the iframe's document
      cy.wrap(iframeDocument).find('#message').should('contain', 'Hello from the iframe!');
      cy.wrap(iframeDocument).find('#change-text-button').click();
      cy.wrap(iframeDocument).find('#message').should('contain', 'Text was changed!');
    });
  });
});

When you run this test, Cypress first visits parent.html. The cy.get('#my-iframe') command finds the <iframe> element on the parent page. The crucial part is .then(($iframe) => { ... }). Inside this callback, $iframe is a jQuery object representing the iframe element.

$iframe.contents() is the magic. It returns a jQuery object representing the document of the iframe. We then chain .find('body') to get the body element within that iframe’s document. This gives us a handle to the content living inside the iframe.

From there, cy.wrap(iframeDocument) is used. iframeDocument is a raw DOM element (or a jQuery object representing it), and cy.wrap is essential for bringing that DOM element back into the Cypress command chain, allowing you to use Cypress commands like .find() and .should() on it. We can then select elements like #message and #change-text-button as if they were on the main page, but they are scoped to the iframe’s content.

The core problem iframes solve is content isolation. They allow you to embed one HTML document within another, and by default, these documents operate in separate browsing contexts. This isolation is great for security and modularity, but it means a test running on the parent page cannot directly access or manipulate elements inside an iframe using standard DOM methods. Cypress needs a specific mechanism to bridge this gap.

The mental model to build is that Cypress commands operate on the current document context. When you cy.visit(), the context is the main page. To interact with an iframe, you must first get a reference to the iframe element itself, then explicitly switch your Cypress context into the iframe’s document. $iframe.contents() is the command that performs this context switch, giving you access to the iframe’s DOM. cy.wrap() is then used to re-enter the Cypress command flow with this new context.

The srcdoc attribute on an iframe allows you to specify the HTML content directly within the iframe tag, rather than linking to an external URL. This is incredibly useful for creating small, self-contained iframe content for testing purposes without needing to serve separate HTML files.

The next concept you’ll likely encounter is handling iframes that load content dynamically or after a user interaction on the parent page.

Want structured learning?

Take the full Cypress course →