22

I have been trying to test a stripe checkout form using cypress.io

If anyone has managed to get this to work please let me know. I found a thread on the matter here https://github.com/cypress-io/cypress/issues/136 and based on this I came up with:

   cy.get('iframe.stripe_checkout_app')
      .wait(10000)
      .then($iframe => {
        const iframe = $iframe.contents()
        const myInput0 = iframe.find('input:eq(0)')
        const myInput1 = iframe.find('input:eq(1)')
        const myInput2 = iframe.find('input:eq(2)')
        const myButton = iframe.find('button')

        cy
          .wrap(myInput0)
          .invoke('val', 4000056655665556)
          .trigger('change')
        cy
          .wrap(myInput1)
          .invoke('val', 112019)
          .trigger('change')

        cy
          .wrap(myInput2)
          .invoke('val', 424)
          .trigger('change')

        cy.wrap(myButton).click({ force: true })
      })

But the problem is that the stripe form still does not register the input values. Here is a little gif of what happens http://www.giphy.com/gifs/xT0xeEZ8CmCTVMwOU8. Basically, the form does not register the change input trigger.

Does anyone know how to enter data into a form in an iframe using cypress?

Josh Pittman
  • 7,024
  • 7
  • 38
  • 66

10 Answers10

25

The following snippet should work for you. I copied/pasted it from @izaacdb's post in this thread.

cy.wait(5000)
cy.get('.__PrivateStripeElement > iframe').then($element => {

  const $body = $element.contents().find('body')

  let stripe = cy.wrap($body)
  stripe.find('.Input .InputElement').eq(0).click().type('4242424242424242')
  stripe = cy.wrap($body)
  stripe.find('.Input .InputElement').eq(1).click().type('4242')
  stripe = cy.wrap($body)
  stripe.find('.Input .InputElement').eq(2).click().type('424')
})

However, in order for the above to work, you need to do the following (copied/pasted from @nerdmax's post from the same thread linked above):

Big Thanks to @Vedelopment @brian-mann !

I tested with react-stripe-checkout component and it works.

Just add some solution details so it may save others some time.

chromeWebSecurity disable:

// cypress.json

{
  "chromeWebSecurity": false
}

--disable-site-isolation-trials:

Check: https://docs.cypress.io/api/plugins/browser-launch-api.html# AND #1951

// /plugins/index.js

module.exports = (on, config) => {
  on("before:browser:launch", (browser = {}, args) => {
    if (browser.name === "chrome") {
      args.push("--disable-site-isolation-trials");
      return args;
    }
  });
};
ptk
  • 6,835
  • 14
  • 45
  • 91
  • https://gist.github.com/mbrochh/460f6d4fce959791c8f947cb30bed6a7#gistcomment-2768523 – Michael Nov 25 '18 at 22:39
  • 1
    I'm only using the `chromeWebSecurity` bit. Did not need the `--disable-site-isolation-trials` – theUtherSide Aug 28 '19 at 21:16
  • 2
    I am using Stripe Elements, and using different selectors (`.__PrivateStripeElement:eq(0) > iframe` with `input[name="cardnumber"]`) was able to get this to work. Also I did need the `chromeWebSecurity` part, and did not need the `--disable-site-isolation-trials` part. – Alan P. Nov 06 '19 at 08:07
  • Can we use XPATH inside the find? Example: find('//div') – Ashok kumar Ganesan Mar 10 '21 at 16:13
  • i love u : ) haha - worked perfectly – sao Nov 05 '21 at 17:18
  • I get the following error with this code using Cypress 12.8.1. "Timed out retrying after 15000ms: cy.find() failed because the page updated as a result of this command, but you tried to continue the command chain. The subject is no longer attached to the DOM, and Cypress cannot requery the page after commands such as cy.find()" – MadMac Mar 29 '23 at 00:20
  • This worked for me: https://stackoverflow.com/a/75883621/10222449 – MadMac Mar 31 '23 at 21:56
5

The '.Input .InputElement' selector from @user8888 did not work for me. Instead, I'm accessing each input by it's name attribute.

        cy.get(".__PrivateStripeElement > iframe").then(($element) => {
                const $body = $element.contents().find("body");

                let stripe = cy.wrap($body);
                stripe
                    .find('[name="cardnumber"]')
                    .click()
                    .type(MOCK_CC_NUMBER);

                stripe = cy.wrap($body);
                stripe
                    .find('[name="exp-date"]')
                    .click()
                    .type(MOCK_CC_EXP);

                stripe = cy.wrap($body);
                stripe
                    .find('[name="cvc"]')
                    .click()
                    .type(MOCK_CC_CVC);

                stripe = cy.wrap($body);
                stripe
                    .find('[name="postal"]')
                    .click()
                    .type(MOCK_CC_ZIP);
            });
theUtherSide
  • 3,338
  • 4
  • 36
  • 35
3

I just spent way too long trying to get this working, none of the answer I found would work completely. I added my solution to the cypress github issue for iframes (there is a bit more context there), also putting it here to hopefully save others some time.

I stole the onIframeReady() function from this stackoverflow answer.

Basically what it is doing is checking if the iframe has loaded, if the iframe has loaded it will do $iframe.contents().find("body"); to switch to the contents. If it has not loaded it will hook that same code into the load event so it will run as soon as the iframe loads.

This is written as a custom command to allow use of cypress chaining after switching to the iframe, so put the following into your support/commands.js file:

Cypress.Commands.add("iframe", { prevSubject: "element" }, $iframe => {
  Cypress.log({
    name: "iframe",
    consoleProps() {
      return {
        iframe: $iframe,
      };
    },
  });

  return new Cypress.Promise(resolve => {
    onIframeReady(
      $iframe,
      () => {
        resolve($iframe.contents().find("body"));
      },
      () => {
        $iframe.on("load", () => {
          resolve($iframe.contents().find("body"));
        });
      }
    );
  });
});

function onIframeReady($iframe, successFn, errorFn) {
  try {
    const iCon = $iframe.first()[0].contentWindow,
      bl = "about:blank",
      compl = "complete";
    const callCallback = () => {
      try {
        const $con = $iframe.contents();
        if ($con.length === 0) {
          // https://git.io/vV8yU
          throw new Error("iframe inaccessible");
        }
        successFn($con);
      } catch (e) {
        // accessing contents failed
        errorFn();
      }
    };

    const observeOnload = () => {
      $iframe.on("load.jqueryMark", () => {
        try {
          const src = $iframe.attr("src").trim(),
            href = iCon.location.href;
          if (href !== bl || src === bl || src === "") {
            $iframe.off("load.jqueryMark");
            callCallback();
          }
        } catch (e) {
          errorFn();
        }
      });
    };
    if (iCon.document.readyState === compl) {
      const src = $iframe.attr("src").trim(),
        href = iCon.location.href;
      if (href === bl && src !== bl && src !== "") {
        observeOnload();
      } else {
        callCallback();
      }
    } else {
      observeOnload();
    }
  } catch (e) {
    // accessing contentWindow failed
    errorFn();
  }
}

Then you would call this like so from your tests:

cy.get('iframe.stripe_checkout_app')
  .iframe()
  .find('input:eq(0)')
  .type("4000056655665556")

You can .alias() after calling .iframe() to refer to it for the rest of your inputs or .get() the iframe several times, I'll leave that up to you to figure out.

Brendan
  • 4,327
  • 1
  • 23
  • 33
3

The solution in this link is working for me. Basically, the steps are as below:

  1. Set chromeWebSecurity to false in cypress.json
  2. Add cypress command to get the iframe in command.js
  3. Use the iframe command in the script
Juniada
  • 66
  • 2
  • Yeah nice first point. I've been looking at variations of solutions for the past hour... failing to disable web security. The `cy.wrap` command worked after that and I was able to send events into the iframe. – Lex May 22 '20 at 03:37
2

This doesn't directly answer your question, but after several days of trying to wrangle manipulating elements in the iframe using jQuery, re-implementing a bunch of stuff that Cypress already did, I smacked myself and started doing this:

Cypress.Commands.add('openiframe', () => {
    return cy.get("iframe[src^='/']").then(iframe => {
        cy.visit(Cypress.$(iframe).attr('src'), { timeout: Cypress.config("pageLoadTimeout") });
    });
});

That allowed me to just cy.openiframe().then(() => {}); and proceed as if the site I was testing didn't put a bunch of functionality in an iframe in the first place.

The downside is that you've got to finish up what you're doing not in the iframe before doing anything in the iframe, so you can't go back and forth too easily.

It might not work for your use case, but if/when it does, it's the easiest workaround I've found.

DrShaffopolis
  • 1,088
  • 1
  • 11
  • 14
2

in order to avoid using:

cy.wait(5000)

I found a better way to do it following the instructions cypress provides in this tutorial about how to work with iframes

   cy.get('iframe[name="__privateStripeFrame5"]')
    .its("0.contentDocument.body")
    .should("not.be.empty")
    .then((body) => {
      cy.wrap(body)
        .find("[name=cardnumber]")
        .type("6011111111111117", { force: true });
   });
Andres Duran
  • 21
  • 1
  • 1
  • I get body null unfortunately, went through their tutorials also. Sometimes it works, but most times ti doesnt – trainoasis Mar 19 '21 at 12:43
  • I think that is related to the use of `[name="__privateStripeFrame5"]` because that could change so instead maybe you can try `cy.get("iframe[title^='a title you use or something']")` that is working for me now. – Andres Duran Apr 16 '21 at 20:22
0

The iframe workflow is still pretty clunky (until this feature is implemented). For now, you can try forcing pretty much every DOM interaction:

cy.visit("https://jsfiddle.net/1w9jpnxo/1/");
cy.get("iframe").then( $iframe => {

    const $doc = $iframe.contents();
    cy.wrap( $doc.find("#input") ).type( "test", { force: true });
    cy.wrap( $doc.find("#submit") ).click({ force: true });
});
dwelle
  • 6,894
  • 2
  • 46
  • 70
  • Thank you. That doesn't seem to work, unfortunately. I tried this and it's still the same. This time the red box is only around the card number input but I still can't submit the form. In case anyone else knows if this is something specific to react-stripe-checkout, here is a quick gif of what happens when I force type http://www.giphy.com/gifs/l2QEaH7zmQooawT6g – Josh Pittman Nov 16 '17 at 17:16
  • Can we use XPATH inside the find? Example: find('//div') – Ashok kumar Ganesan Mar 10 '21 at 05:04
0

I released a plugin yesterday that adds a simple Cypress API for filling in Stripe Elements:

cy.fillElementsInput('cardNumber', '4242424242424242');

This plugin avoids cy.wait() calls, peeking into <iframe>s manually, and other awkward selectors.

dbalatero
  • 158
  • 1
  • 5
0

For me, this iframe has a random behavior, so I used some of the code, but finally tried with force: true because the last value was failing. The name was not valid for me, I had to use a different attribute which is more specific.

    cy.get(".__PrivateStripeElement > iframe").then(($element) => {
        const $body = $element.contents().find("body");

    cy.wrap($body)
        .find('[data-elements-stable-field-name="cardNumber"]')
        .click()
        .type('value')

    cy.wrap($body)
        .find('[data-elements-stable-field-name="cardExpiry"]')
        .click()
        .type('value')

    cy.wrap($body)
        .find('[data-elements-stable-field-name="cardCvc"]')
        .click()
        .type('value', { force: true });
Neha Soni
  • 3,935
  • 2
  • 10
  • 32
JVT
  • 1
0

The other answers didn't work for me because the name attribute has changed. Here's what I used:

cy.get('.__PrivateStripeElement > iframe').then(($element) => {
      const $body = $element.contents().find('body')

      let stripe = cy.wrap($body)
      stripe.find('input[name="number"]').click().type('4242424242424242')
      stripe = cy.wrap($body)
      stripe.find('input[name="expiry"]').click().type('4242')

      stripe = cy.wrap($body)
      stripe.find('input[name="cvc"]').click().type('424')

      stripe = cy.wrap($body)
      stripe.find('input[name="postalCode"]').click().type('92222')
})
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Scott
  • 944
  • 1
  • 6
  • 9
  • This doesn't work for me. See attempt 3 https://stackoverflow.com/questions/75882883/cypress-12-8-1-not-working-with-stripe-elements-iframe – MadMac Mar 29 '23 at 23:34