Open brian-mann opened 8 years ago
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
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?
Target the iframe, use a should
function and then query inside of it until it returns you a DOM node that you expect to eventually be there.
cy.get('iframe', { timeout: 20000 }).should(($iframe) => {
expect($iframe.contents().find('the-new-dom-element')).to.exist
})
.then(($iframe) => {
return cy.wrap($iframe.contents().find('the-new-dom-element'))
})
CypressError: Timed out retrying: chainers.split is not a function. Do I need to pass the timeout object parameter explicitly?
Hah sorry - I wrote that code blind, and the timeout
should have gone on the cy.get
, not the assertion. Noob mistake - maybe we should take that situation into account. I've updated the code to reflect the change.
@harsh2602 personally I'd just run that iframe code I had to get it a 2nd time after you know the url of the iframe changes. I personally avoid changing the timeout because it is hard to explain rules around what that number should be, and feels similar to setting an arbitrary wait time (though certainly much better than that)
cy.get('#foo').iframe().find('.change-url').click();
cy.get('#foo').iframe().find('.new-thing');
+1 for this issue; Cypress looks great and it was our first choice to use for our automation tests because of features like the video recording, and how easy it was to get going with... however, we have a lot of iframes in legacy parts of our application, so we had to go with a competitor. Please get this sorted, because if it wasn't for this Cypress would be my strong preference, but it's a show-stopper.
@RichardW1001 did the workarounds listed above not work for you?
For those using CKEditor, I struggled to get the workaround to work for me as there is no input field within the iframe on which to issue type(). In the end, I managed to get my tests working by doing the following:
cy.window().then(win => {
win.CKEDITOR.instances.myEditableField.setData('<p>Foo</p>')
})
Replacing myEditable field with the id of the editable field (CKEditor 4).
+1, it would be good to be able to access iframe content
Cross-origin iframes would be an awesome feature for me, since I use Stripe Checkout, which loads a cross-origin iframe to take a user's credit card number. It's pretty hard to test the full payment flow without being able to query/trigger events on that credit card form.
@isaaclyman just set chromeWebSecurity: false
and then you can access those iframes and then wrap the elements and trigger events on them with Cypress.
This is my implementation to work with an iframe (a bit of a mixture of the above versions):
// Find iframe with selector. Wait for element to exist inside the iframe
// return all body
Cypress.Commands.add('iframe', (selector, element) => {
return cy.get(`iframe${selector || ''}`, { timeout: 10000 })
.should(($iframe) => {
expect($iframe.contents().find(element || 'body')).to.exist
}).then(($iframe) => {
var w = cy.wrap($iframe.contents().find("body"));
// optional - add a class to the body to let the iframe know it's running inside the cypress
// replaces window.Cypress because window.Cypress does not work from inside the iframe
w.invoke('addClass', 'cypress');
return w;
});
});
You can easily setup an alias so you can later on work with it:
cy.iframe('', 'div.page-host').as('iframe');
cy.get("@iframe").find("div.someclass").should("exist");
You can also give a name or id
cy.iframe('#iframeid', 'body').as('iframe');
cy.iframe('[name=iframename]', 'body').as('iframe');
I'm trying verify the text of an h1 tag inside of an iframe like below, but it is not working. I've also tried .text() and .get(), and several variations, but I'm not able to get it to work. I've wrapped the expectation in a setTimeout as well, but does not work.
Any advice @brian-mann?
cy.get("iframe", { timeout: 20000 }).should($iframe => {
expect(
$iframe
.contents()
.find("h1")
.get(0).innerText,
).to.be("The Header");
});
@kevinold You're not actually writing an assertion in chai
because to.be
is not a function and doesn't do anything.
You want .to.eq("The Header")
A sidenote: I needed to add a workaround to @corneliutusnea's version to detect iframe navigation that happens just before the cy.iframe()
call. My code was failing this way:
cy.get('something in window.top').click() // navigates the iframe
cy.iframe('.my-frame').as('iframe') // this sometimes grabbed the OLD document the frame was in before navigation
cy.get('@iframe').find(...) // ... FAIL
Now I use:
cy.discardIframe('.my-frame') // marks the old frame document as stale
cy.get('something in window.top').click() // navigates the iframe
cy.iframe('.my-frame').as('iframe') // waits for the navigation to occur
cy.get('@iframe').find(...) // ... GREAT SUCCESS
The implementation:
Cypress.Commands.add('discardIframe', selector => {
cy.get(`iframe${selector || ''}`).then(f => {
f.contents()[0].__STALE = true
})
})
Cypress.Commands.add('iframe', (selector, element) => {
return cy
.get(`iframe${selector || ''}`, {timeout: 10000})
.should($iframe => {
expect($iframe.contents()[0].__STALE).to.be.undefined
expect($iframe.contents().find(element || 'body')).to.exist
})
.then($iframe => {
var w = cy.wrap($iframe.contents().find('body'))
// optional - add a class to the body to let the iframe know it's running inside the cypress
// replaces window.Cypress because window.Cypress does not work from inside the iframe
w.invoke('addClass', 'cypress')
return w
})
})
I'm eager to hear if there are easier workarounds.
@corneliutusnea your workaround functions correctly, thanks. Unfortunately, there's a problem with debugging... I don't see a preview of the content in the iframe. Any way to rectify this?
@ryan-mulrooney I'm not sure I understand your issue. You don't see the preview of the content of the iframe loaded inside the test browser window? Is your iframe loading slow(er)?
Maybe add the second selector so there is a wait until the iframe contents loads:
cy.iframe('', 'div.page-host').as('iframe');
The div.page-host
there should be a selector to some element that you know exists only once the contents is correctly loaded otherwise the iframe that is loaded and wrapped is the "old" iframe.
If the contents of the iframe changes you might need to use @tuomassalo trick to discard and reload the contents of the iframe inside cypress.
@corneliutusnea Sorry, i should have been clearer. When the test is running the iframe content is displayed in the test runner fine. It's time travelling (after the test has ran) that's the problem. When I hover over any of my test steps i see an empty iframe with <iframe> placeholder for
displayed. For example:
@ryan-mulrooney This behavior is intentional. You can read implementation details in original issue here: https://github.com/cypress-io/cypress/issues/234
This should probably be clearer in our documentation.
@jennifer-shehane would be nice if iframes are also stored in snapshots (Useful for running storybook tests with cypress)
@xfumihiro Could you open an issue for this specifically? This is very different than the feature outlined in this issue. Thanks!
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 itsparent
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
<iframe>
.{ chromeWebSecurity: false }
(Chromium-based browsers only).Examples of how we could do this
Workaround
It's possible to run
cy.*
commands on iframe elements like below:⚠️ 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
Same-origin iframe
Example where a site uses a same-domain iframe as a date-picker widget
We also intend to support snapshots of iframes.