cypress icon indicating copy to clipboard operation
cypress copied to clipboard

Iframe support

Open brian-mann opened this issue 8 years ago • 430 comments

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.

brian-mann avatar May 11 '16 13:05 brian-mann

commenting that I care because the docs said I should and we need this functionality for full test coverage.

Courey avatar May 31 '16 15:05 Courey

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.

acusti avatar Jul 21 '16 21:07 acusti

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.

brian-mann avatar Jul 21 '16 21:07 brian-mann

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!)

acusti avatar Jul 22 '16 02:07 acusti

same domain iframe would allow us to test emails via e.g. mailinator.com

maximilianschmid avatar Sep 21 '16 11:09 maximilianschmid

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
})

brian-mann avatar Sep 21 '16 13:09 brian-mann

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 avatar Oct 14 '16 07:10 harz87

@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.

brian-mann avatar Oct 14 '16 14:10 brian-mann

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:

screen shot 2017-02-06 at 2 41 03 pm

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.

rbone avatar Feb 06 '17 03:02 rbone

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.

oliver3 avatar Feb 14 '17 14:02 oliver3

+1 We are also integrating external (cross-domain) payment methods and would like to test.

afohlmeister avatar Feb 17 '17 14:02 afohlmeister

+1 We are also integrating external (cross-domain) payment methods

mvandebunt avatar Apr 06 '17 12:04 mvandebunt

People writing about payment methods, so do i. I'm trying to pass Stripe checkout iframe

cypress.json
{
    "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
})

image

But after click: image

Manually filled form looks formatted image

Anyone knows how to resolve this?

Alex0007 avatar Jun 16 '17 17:06 Alex0007

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')
})

brian-mann avatar Jun 16 '17 17:06 brian-mann

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 "

caerie4 avatar Jun 22 '17 19:06 caerie4

Are you still planning to add support for this later?

caerie4 avatar Jun 22 '17 19:06 caerie4

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.

brian-mann avatar Jun 22 '17 19:06 brian-mann

This would be great for us too, we are using CKeditor quite a bit (in an iFrame)

AshMcConnell avatar Aug 11 '17 15:08 AshMcConnell

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.

bd82 avatar Aug 27 '17 11:08 bd82

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.

screen shot 2017-09-08 at 9 14 41 am screen shot 2017-09-08 at 9 14 28 am

brian-mann avatar Sep 08 '17 13:09 brian-mann

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?

paulfalgout avatar Nov 02 '17 18:11 paulfalgout

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.

brian-mann avatar Nov 02 '17 18:11 brian-mann

You'd likely need to write a custom command that takes this into account. It checks document and polls it until its ready

brian-mann avatar Nov 02 '17 18:11 brian-mann

ok that was the direction I was headed :+1:

paulfalgout avatar Nov 02 '17 18:11 paulfalgout

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

});

ioan-ghisoi avatar Nov 03 '17 11:11 ioan-ghisoi

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 avatar Nov 07 '17 06:11 paulfalgout

@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 avatar Nov 14 '17 21:11 jusefb

@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 .finding against.. so clearly .find('body') isn't going to work.

paulfalgout avatar Nov 15 '17 05:11 paulfalgout

I get it now, thank you very much for your help

jusefb avatar Nov 15 '17 11:11 jusefb

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?

harsh2602 avatar Dec 07 '17 19:12 harsh2602