Cypress tips and tricks, part 1 : working with single-purpose iframes

Cypress Logo

Cypress is a fantastic test automation tool for frontend testing newbies. It has a low barrier to entry, with detailed and helpful documentation. It comes bundled with many of the key components of a full-fledged testing framework, minimising initial configuration. Most importantly, it’s enjoyable to work with, and has a strong community of enthusiastic and helpful users behind it. It’s also quite easy to integrate into CI/CD pipelines, as demonstrated in my previous Cypress post.

In this blog post series I’ll share some tips and tricks for extending Cypress with some clever JS wizardry. All of these solutions are based on my real-world use of the tool on a complex greenfield project.

This post is part 1 of 3. I’ll provide a brief overview of Cypress’ strengths and weaknesses, then share my first tip for interacting with single-purpose iframes. I’ll update this post with links when parts 2 and 3 are published.

Benefits of adopting/using Cypress

For me, the key benefits of using Cypress include:

  • It has excellent documentation, with helpful use cases and code samples for each of their commands.
  • It bundles all of the usual features of a full testing framework in a single package. All of Cypress’ capabilities are accessible via the default Cypress commands.
    • Most Selenium-based frameworks require you to pick your own components to build up the full framework.
    • For example, you might choose Mocha, Jasmine or Cucumber to structure your tests and handle test reporting, then Chai or Expect for assertions.
    • For API testing you might include a library such as SuperTest or REST Assured.
    • When working with HTTP requests you might use nock or WireMock.
    • To test using mocks, stubs and spies you might use Sinon or Mockito.
    • This build-your-own approach is great for those experienced in automation because of the flexibility it offers. However it can be very overwhelming to newcomers.
    • Cypress is bundled with Mocha and Chai, so you don’t have to pick and configure these key components. It also has a range of built-in commands that provide API testing, networking and mocking capabilities.
  • It doesn’t require much prior knowledge of programming concepts or JavaScript. Most tests involve simply stringing together Cypress commands to get the desired outcome. Still, it’s quite easy to extend Cypress’ capabilities with small JS snippets.
  • It has some powerful commands that enable both end-to-end testing and UI only (mocked backend) testing. Useful commands in this area include intercept, stub, spy, wait and fixture.
  • It has a decent library of plugins that you can use to add extra functionality, integrate your tests with specific technologies or structure your tests.

Drawbacks

No tool is perfect, of course. Cypress is very easy to get started with, but you’ll quickly hit a brick wall if you need to deal with any of the following:

Cypress’ docs provide a full list of known limitations. You can circumvent many of these with clever strategies or third-party plugins. Nonetheless, to avoid these workarounds you should consider competing tools such as Playwright or WebdriverIO that don’t have these issues.

Cypress Tip #1: Jumping into single-purpose iframes

Cypress’ best known limitation is probably lack of iframe support. The test runner works its magic by injecting itself into the browser context using JavaScript. This injection does not happen within iframes. There are plugins and custom commands you can add to provide better iframe capabilities. But here’s a quick hack for performing very simple actions within single-purpose iframes.

On my current e-commerce project, one of our payment methods uses Hosted Fields to securely collect card details and submit them to the payment provider without this information ever touching our servers. These hosted fields are essentially tiny iframes, hosted by your payment provider, that you embed into the checkout page.

A hosted fields illustration, overlaid over some example JavaScript code.
Hosted Fields illustration, courtesy of Braintree’s official documentation

The solution

To integrate this type of iframe into a Cypress test, we can make use of the .then() and .wrap() commands to briefly circumvent Cypress’ iframe blindness.

For this solution to work, you will probably need to set set chromeWebSecurity to false in your cypress.json config file. This means that tests that rely on this config won’t pass in Firefox – I’ll cover this in part 2 of this blog post series.

Here’s the re-usable page object from our checkoutPage page object class:

fillInCheckoutCardField(iframeId, fieldId, fieldValue) {
    cy.get(iframeId)
      .its('0.contentDocument.body')
      .should('not.be.empty')
      .then(body => {
        cy.wrap(body).find(fieldId).type(fieldValue)
      })
  }

Here’s how this snippet works:

  • First, the .its() command is used to retrieve the body content of the iframe.
  • Next, the .should('not.be.empty') assertion ensures that the next command won’t run until the iframe has properly initiated.
  • Finally, within the .then() command, the iframe body is wrapped so that we can use Cypress commands to find the specified input field and type within it.

And this is how you can initiate the page object in a spec file:

checkoutPage.fillInCheckoutCardField(
          checkoutPage.creditCardFrameId,
          checkoutPage.creditCardNumberId,
          faker.datatype.number()
        )

Note: We’ve used the faker library here to generate a random number that will fail frontend validation, and the checkoutPage.[...]Id variables are defined in the constructor method of the page object class.

Once you’ve added this page object to your codebase, you can use it to type anything into any iframe! Similar page objects could click a specific button, or press enter immediately after typing in order to submit a form. For example, the below snippet types a one-time password into a 3D Secure iframe, then presses the Enter key:

// within page object class:
fillInOtpField(iframeId, fieldId, fieldValue) {
    cy.get(iframeId)
      .its('0.contentDocument.body')
      .should('not.be.empty')
      .then(body => {
        cy.wrap(body).find(fieldId).type(`${fieldValue}{enter}`)
      })
  }

// within test spec file:
 checkoutPage.fillInOtpField(
          checkoutPage.otpIframeID,
          checkoutPage.otpFieldId,
          '1234'
        )

Caveats

I must stress that this technique is unreliable and unwieldy when performing multiple actions within a single iframe. You will likely need to refetch the iframe contents after every action, and you risk falling into async hell.

Many externally-hosted iframes are shockingly slow to load, so they might not be ready when you try to interact with them. You can partially deal with this by inserting a conditional wait (using the intercept, as and wait commands), for example:

cy.intercept(
      'POST',
      'https://externalsite.com/some/api/endpoint'
    ).as('3dSecureAuth')

cy.wait('@3dSecureAuth')

If all else fails, use a dreaded explicit wait to give the external iframe some extra time to catch up. It’s not good practice, but sometimes needs must.

Resources

I hope you’ve found this post useful. Here’s some related resources:

One thought on “Cypress tips and tricks, part 1 : working with single-purpose iframes

  1. […] Cypress tips and tricks, part 1 : working with single-purpose iframes – James Sheasby Thomas […]

Leave a Reply