Cypress: Targeting elements inside iframes
Cypress is an automated testing tool that allows you to write end-to-end tests for your app. It has a fantastically easy-to-use, jQuery-like syntax and is a powerful and delightful tool for your integration tests.
One major pain point when using Cypress is testing on elements within iframes. Unfortunately,
Cypress can’t easily target elements within an iframe without a bit of extra work.
Worse, their documentation on doing so is a sad paragraph in their FAQ so you’ll wind up combing through loads of GitHub Issues (for extra reading this issue is the best you'll find).
This post outlines some lessons learned and some simple Cypress custom commands that make working
with iframes a breeze.
Disable chromeWebSecurity
First, if your iframe uses cross-origin resources (ex. loading a script from a 3rd party website
like Plaid or Stripe), you need to update your cypress.json and disable chromeWebSecurity.
{
// ...Other settings
"chromeWebSecurity": false
}Without this change, your iframe assets will not load due to CORS (cross-origin resource sharing)
issues.
Selecting iframes in Cypress
The simplest method to target elements within iframes is to simply use Cypress's then method.
then yields the element from the previous command.
cy.get('iframe').then($iframe => {
const $body = $iframe.contents().find('body')
cy.wrap($body).find('input').type('fake@email.com')
})The issue with this approach is it doesn't ensure that the content within the iframe is available
yet. For example, this approach may test errors because the body element wasn't found when you
attempted to run find('input').
To ensure that the iframe has loaded, we need a little more legwork:
cy
.get('iframe')
.should(iframe => expect(iframe.contents().find('body').to.exist)
.then(iframe => cy.wrap(iframe.contents().find('body')))
.within({}, $iframe => {
cy.get('input').type('fake@email.com')
})The primary improvement is the should method. should ensures that the iframe has a body
before we attempt to run commands on it. Additionally, instead of returning the iframe's body to
work off of, we can scope subsequent calls within the body element using within.
Clean it up with a Cypress custom command
The code above it pretty gnarly. We can greatly DRY up the code by extracting it into a reusable Cypress command:
Cypress.Commands.add('iframe', { prevSubject: 'element' }, ($iframe, callback = () => {}) => {
// For more info on targeting inside iframes refer to this GitHub issue:
// https://github.com/cypress-io/cypress/issues/136
cy.log('Getting iframe body')
return cy
.wrap($iframe)
.should(iframe => expect(iframe.contents().find('body')).to.exist)
.then(iframe => cy.wrap(iframe.contents().find('body')))
.within({}, callback)
})This command utilizes the previously found element, then ensures that the iframe has finished
loading before scoping the iframe body for subsequent calls.
Usage:
cy.get('iframe').iframe(() => {
// Targets the input within the iframe element
cy.get('input').type('fake@email.com')
})Clean code, baby!
Timeouts
Don’t be afraid to set pretty high timeouts when working with iframes. There’s a lot of room for
high latency because:
iframes load a separate HTML document, which takes time to parse, execute, and render.iframeassets don’t share the same cache as your app. This means every asset is downloaded as if were for the first time.iframes often load 3rd party scripts. In addition to downloading, parsing, and executing, this 3rd party script may take time to initialize dynamic content.- The client’s network conditions may vary from you and your 3rd party’s servers.
When testing iframes loaded by 3rd party scripts, I often reach for timeouts like so:
cy.get('iframe').iframe(() => {
cy.find('input', { timeout: 10 * 1000 }).type('fake@email.com')
})The code above will look inside the iframe for the input element. If it finds the element, it
will resolve immediately and move on to the type method. Otherwise, it will wait 10sec before
erroring.
Note: Using timeout within Cypress commands is preferable to the generic cy.wait. Unlike
timeout , cy.wait will pause execution for the entirety of the time regardless of whether the
element is found or not. As a result, cy.wait tends to be much more brittle and cause intermittent
failures.
// postMessage
When working with iframes, we sometimes use
postMessage to facilitate
communication between the iframe and our app.
One helpful use case is to notify the app that the iframe content has finished loading dynamic
content and is ready to receive user input.
For example, you might use the postMessage API to send your app a ready event after a few seconds.
<!-- Inside the iframe -->
<script>
setTimeout(() => {
window.parent.postMessage({ code: 'Ready' }, e.origin)
}, 3 * 1000)
</script>
// Inside your app
window.addEventListener('message', e => {
var data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data
var code = data.code
if (code === 'Ready') {
alert('iframe is ready')
}
})If you use the postMessage API, using the command from the previous section may have a race
condition prone to intermittent failures.
Our custom iframe Cypress command only checks that the iframe element is in the ready state, but
that may not include dynamically rendered content.
If you have this issue, you can create another Cypress custom command isIFrameReady to listen for
the Ready event sent from your iframe.html.
Cypress.Commands.add('isIFrameReady', () => {
return cy.window().then({ timeout: 10 * 1000 }, window => {
return new Cypress.Promise(resolve => {
window.addEventListener('message', e => {
const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data
if (data.code === 'Ready') {
resolve()
}
})
})
})
})And using our two commands together:
cy.isIFrameReady().then(() => {
cy.get('iframe').iframe(() => {
cy.get('input', { timeout: 10 * 1000 }).type('fake@email.com')
})
})Wrapping up
Working with iframes is a headache in Cypress, but I hope this post helps you overcome common
issues I ran into with them.
Feel free to leave questions below.
Thanks for reading! You are my favorite person for sticking around until the end. 🍻
This blog is a constant work in progress, and I want to get better with your help! If you have feedback or questions on this post, please leave a comment below or reach out to me on Twitter.
This article is from nicknish.co where I publish articles on software engineering and how to leverage technology to build products that people will pay you for.
Thanks for reading! You are my favorite person for sticking around until the end. 🍻
This blog is a constant work in progress, and I want to get better with your help! If you have feedback or questions on this post, please leave a comment below, use my site's contact page, or reach out to me on Twitter.