cypress
cypress copied to clipboard
Iframe support
Updated Feb 9, 2021 - see note below or https://github.com/cypress-io/cypress/issues/136#issuecomment-773765339
Currently the Test Runner does not support selecting or accessing elements from within an iframe.
What users want
Users of Cypress need to access elements in an <iframe>
and additionally access an API to "switch into" and switch back out of different iframes. Currently the Test Runner thinks the element has been detached from the DOM (because its parent
document is not the expected one).
What we need to do
-
Add new
cy
commands to switch into iframes and then also switch back to the "main" frame. -
Cypress must inject itself into iframes so things like XHR's work just like the main frame. This will ideally use something like
Mutation Observers
to be notified when new iframes are being pushed into the DOM. -
[ ] Add API to navigate between frames
-
[ ] Update the Driver to take into account element document references to known frames
Things to consider
- How will we handle snapshotting? Currently we don't take snapshots for anything inside of an
<iframe>
. - How will we handle cross origin frames? It's possible to enable these with
{ chromeWebSecurity: false }
(Chromium-based browsers only). - How will we show context switching in the Command Log? It should probably look / be colored differently than the 'normal' main commands
Examples of how we could do this
// switch to an iframe subject
cy
.get('#iframe-foo').switchToIframe()
.get('#button').click() // executed in <iframe id='iframe-foo' />
// or pass in $iframe object in hand
cy
.get('#iframe-foo').then(($iframe) => {
cy.switchToIframe($iframe)
cy.get('#button').click()
})
// now switch back to the main frame
cy
.switchToMain()
.get(':checkbox').check() // issued on the main frame
Workaround
It's possible to run cy.*
commands on iframe elements like below:
cy.get('iframe')
.then(($iframe) => {
const $body = $iframe.contents().find('body')
cy.wrap($body)
.find('input')
.type('[email protected]')
})
⚠️ Updates
Updates as of Feb 9, 2021
Pasting some snippets from our technical brief on iframe support that we are currently planning. As always, things can change as we move forward with implementation, but this is what we are currently planning.
If there's any feedback/criticism on these specific proposed APIs, we'd be happy to hear it.
.switchToFrame([...args], callback)
Switches into an iframe and evals callback in the iframe. Doesn’t matter whether the iframe is same-origin or cross-origin.
Stripe payment example
// ❗️ This is planned work and does not currently work
cy.visit('someshop.com')
// ... add stuff to cart
// ... get to payment page
cy.get('iframe').switchToFrame(() => {
cy.get('#name').type('name')
cy.get('#number').type('1234-5678...')
cy.contains('Pay').click()
})
// go on with cypress commands in the main frame
cy.contains('Thanks for your order')
Same-origin iframe
Example where a site uses a same-domain iframe as a date-picker widget
// ❗️ This is planned work and does not currently work
cy.visit('https://date-picker.com')
cy.get('iframe').switchToFrame(() => {
cy.get('.next-month').click()
cy.contains('24').click()
})
// switch out of iframe context because callback is finished
cy.get('.date').should('have.text', '2/24/2021')
We also intend to support snapshots of iframes.
commenting that I care because the docs said I should and we need this functionality for full test coverage.
We also would need this functionality to be able to really use Cypress. Our app relies on rendering into an iframe for a significant part of it‘s functionality (it’s an editor, and the iframe is used to sandbox the thing being edited), and being able to target elements in that iframe is pretty essential to our testing.
Is the iframe
same domain or cross domain?
We can pretty easily add same domain iframe support. Cross domain will take a lot more work. The difference will likely be days of work vs 1-2 weeks.
Same domain. The iframe is really just a sandboxed canvas that we render into, rather than a 3rd party resource that we are loading into the app. (And thanks for the quick reply!)
same domain iframe would allow us to test emails via e.g. mailinator.com
Cypress actually injects to forcibly enable it to access same domain <iframes>
and even sub-domain <iframes>
but there is an artificial limitation in the driver code where it get's confused when elements are returned and they're not bound to the top frame of your application.
This is coming up on our radar and it will introduce a few more API's to enable switching in and out of <iframes
>.
As a workaround today you can actually still target these elements and potentially perform actions on them by you cannot return them or use them in cypress commands.
So this actually works:
cy.get("iframe").then(function($iframe){
// query into the iframe
var b = $iframe.contents().find("body")
// you can work with this element here but it cannot
// be returned
var evt = new Event(...)
b.dispatchEvent(evt)
return null
})
I would need that feature too. To really test all use cases of our credit card from which is implemented by using braintree hosted fields api. They inject iframes into div and do the complete validation. However to test that our visuals and the submission to the server works I would need to be able to access those iframes.
@harz87 the braintree
iframes you're referring to are cross origin frames right?
If that's the case you'll need to disable web security before being able to access them. After you do that you can at least manually interact with them and force their values to be set - although you won't be able to use Cypress API's until we add support for switching to and from iframes.
This is something that'd be pretty important for my company. We've currently got a working ruby+selenium testing setup, however not all of our company is able to take advantage of those tools as we have some PHP and Go codebases, each of which is rapidly accumulating more and more JS heavy interfaces we'd like to be able to test. We're looking at cypress as a possible candidate to standardise on, but iframe support is currently a blocker.
The specific test case we're evaluating is our payments flow, which loads a modal containing an iframe on the same domain with all of the controls for inputting payment details and submitting requests to pay to the server.
Here's a screenshot to give you a better idea:
All of the content you can see is actually embedded in an iframe. Our tests go through the various payment methods we offer, filling in details like credit card information, billing address, etc. before clicking pay and then asserting that an appropriate message is displayed such as "Payment successful" or an appropriate error message. Unfortunately we can't just test the content within the iframe directly as the payment modal is designed to be embedded into a page, and it communicates necessary information between the page containing the iframe and the page within the iframe.
We can use the workaround posted above, however as we're aiming to replace an already functional test system it makes it harder to justify using the workaround when it greatly reduces the benefits of using cypress. Let me know if there's anymore information you need.
We would like to have this feature in cypress, because we are using CKEditor for wysiwyg input in our application, and it uses an iframe.
+1 We are also integrating external (cross-domain) payment methods and would like to test.
+1 We are also integrating external (cross-domain) payment methods
People writing about payment methods, so do i. I'm trying to pass Stripe checkout iframe
{
"chromeWebSecurity": false
}
.get('iframe.stripe_checkout_app').should('exist')
.then(function ($iframe) {
const iframe = $iframe.contents()
const cardNumInput = iframe.find('input:eq(0)')
const cardValidInput = iframe.find('input:eq(1)')
const cvcInput = iframe.find('input:eq(2)')
cardNumInput.val('4242424242424242')
cardValidInput.val('1222')
cvcInput.val('123')
setTimeout(() => {
iframe.find('button').click()
}, 1000)
return null
})
But after click:
Manually filled form looks formatted
Anyone knows how to resolve this?
EDITED: to show using proper Cypress commands.
.get('iframe.stripe_checkout_app')
.then(function ($iframe) {
const $body = $iframe.contents().find('body')
cy
.wrap($body)
.find('input:eq(0)')
.type('4242424242424242')
cy
.wrap($body)
.find('input:eq(1)')
.type('1222')
cy
.wrap($body)
.find('input:eq(2)')
.type('123')
})
We use Iframe to insert user made forms into a webpage. We have to look at whats entered in the fields. The forms don't even show up in the cypress browser. They are just replaced with "
Are you still planning to add support for this later?
We don't show content in iframes when reverting to a snapshot (nor will we ever do that).
However we will eventually support targeting DOM elements inside of iframes.
This would be great for us too, we are using CKeditor quite a bit (in an iFrame)
I'm also very interested in the ability to target elements in an iframe (same domain). The group I'm part of are building a Web IDE and during testing the whole application "instance" runs in an iframe for isolation purposes.
This means that the root frame is only responsible for starting the inner frame application and then issuing commands.
This works great for our unit and integration testing using karma. I would very like to explore cypress as an alternative for our flaky E2E selenium tests. But without iframe targeting 95% of our use case becomes irrelevant.
We are much closer to having this work. Here in 0.20.0
you are able to wrap
We'll still need to build in API's that enable you to switch the document context to the iframe, so you can use the querying methods of Cypress the same as you do on the outer document. Also things like verifying actionability still need work - notice I need to pass { force: true }
to get the click to work.
Nevertheless its a big step forward because you can at least now fill out forms and interact with iframe elements, which prior to 0.20.0
did not work at all.


So this does work, but I find I have to add a wait(5000)
prior to the then
otherwise getting the iframe contents will be the about:blank
while the frame is loading. since the iframe request is not XHR I suspect there's no way to route
it with a wait
alias? any other suggestions than an arbitrary wait time?
No, this is all part of the complexity of support iframes. Essentially all of the same safeguards we've added to the main frame have to be replicated on iframes.
We do a ton of work to ensure document
and window
are current - we listen for load
events and unload
events to pause and resume command execution - all of that has to be implemented for frames.
The additional complexity is that we can't add those listeners until you've told us you want to "switch" into those frames.
You'd likely need to write a custom command that takes this into account. It checks document
and polls it until its ready
ok that was the direction I was headed :+1:
This is an option typing in an input field situated in the iframe:
cy.get(iframe_selector).then($iframe => {
const iframe = $iframe.contents();
const myInput = iframe.find("your input selector like #myElement");
cy.wrap(myInput).type("example");
//you don't need to trigger events like keyup or change
});
This is what I ended up doing. Seems to work rather well. You can alias the iframe()
method, but if anything in the iframe loads another URL you have to do the original get().iframe()
again.
Cypress.Commands.add('iframe', { prevSubject: 'element' }, $iframe => {
return new Cypress.Promise(resolve => {
$iframe.on('load', () => {
resolve($iframe.contents().find('body'));
});
});
});
// for <iframe id="foo" src="bar.html"></iframe>
cy.get('#foo').iframe().find('.bar').should('contain', 'Success!');
@paulfalgout could you explain what do you mean by "but if anything in the iframe loads another URL you have to do the original get().iframe() again"? Thank you I seem to have trouble switching between multiple iframes
@jusefb So for the same reason an async load is needed to wait until the iframe contents resolve, if the contents of the iframe changes urls (not the #hash but a full page load) you need that same async on('load'
to be able to safely find the contents again.
I should also note that the above returns the body
for .find
ing against.. so clearly .find('body')
isn't going to work.
I get it now, thank you very much for your help
I have a scenario as follows: Step I: In the iframe pass user id and click continue to submit a form Step II: It takes me to a new form within the same iframe and the old form is destroyed from the DOM
If the iframe custom method uses 'element' as prevSubject (as suggessted in the solution by @paulfalgout ), i can traverse the parent and child nodes in the DOM in Step I. In Step II, since I've lost my old DOM structure I cannot traverse to the new form fields as it requires the last used element to find the new element.
Is there a way to get past this problem?