Cypress 12.8.1 not working with Stripe Elements iframe
Asked Answered
G

2

13

I have been stuck on this for ages and can't figure out how to get Cypress 12.8.1 to work with Stripe elements to enter credit card details and create a payment.

I have scoured the internet but none of the solutions seem to work.

Any help is greatly appreciated.

I have tried:

  1. Using xpath https://www.browserstack.com/guide/frames-and-iframes-in-cypress this does nto work for me. See error: https://github.com/cypress-io/cypress/issues/24764#issuecomment-1489438851 enter image description here

  2. Tried this plugin but it does not work anymore. https://mcmap.net/q/590144/-how-to-get-stripe-element-in-cypress-duplicate https://github.com/dbalatero/cypress-plugin-stripe-elements

  3. Tried this but got the following error.

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

enter image description here

  1. Tried a few versions of adding the custom Cypress command "iframeLoaded" but I can't figure out how to add these in the new Cypress 12 typescript format and just get errors. https://medium.com/@michabahr/testing-stripe-elements-with-cypress-5a2fc17ab27b https://bionicjulia.com/blog/cypress-testing-stripe-elements

My code in support/commands.ts

// ***********************************************
// This example namespace declaration will help
// with Intellisense and code completion in your
// IDE or Text Editor.
// ***********************************************
declare namespace Cypress {
  interface Chainable<Subject = any> {
    iframeLoaded($iframe: any): typeof iframeLoaded;
  }
}

function iframeLoaded($iframe: any): Promise<any> {
  const contentWindow = $iframe.prop('contentWindow')
  return new Promise(resolve => {
    if (contentWindow && contentWindow.document.readyState === 'complete') {
      resolve(contentWindow)
    } else {
      $iframe.on('load', () => {
        resolve(contentWindow)
      })
    }
  })
}

Cypress.Commands.add('iframeLoaded', {prevSubject: 'element'}, iframeLoaded);

enter image description here

Update:

I think I have it using Fody's answer. I made 3 changes. I had to change it like so:

    function getCardField(selector: any, attempts = 0) {
          Cypress.log({displayName: 'getCardField', message: `${selector}: ${attempts}`})
          
          if (attempts > 50) throw new Error('too many attempts')
        
          return cy.get('iframe', {timeout:10_000, log:false})
// CHANGE: .eq(1 to .eq(0
            .eq(0, {log:false})
            .its('0.contentDocument', {log:false})
            .find('body', {log:false})
            .then(body => {
              const cardField = body.find(selector)
              if (!cardField.length) {
                return cy.wait(300, {log:false})
                  .then(() => {
                    getCardField(selector, ++attempts)
                  })
              } else {
                return cy.wrap(cardField)
              }
            })
        }
        
// CHANGE: "div.CardField" to "div.CardNumberField input"
        getCardField('div.CardNumberField input')
          .type('4242424242424242')

// CHANGE: "div.CardField" to "div.CardNumberField-input-wrapper"
getCardField('div.CardNumberField-input-wrapper')
      .find('input').eq(0)
      .should('have.value', '4242 4242 4242 4242')   // passes
Gwalior answered 29/3, 2023 at 23:6 Comment(0)
N
16

The short answer is, the Stripe iframes take time to load and display the fields you need to access, so you need to add a retry.

Usually you use .should() assertions to retry until something you want is present in the DOM.

Unfortunately, with an <iframe> inside the page, you can't use .should() because it doesn't retry all the steps in the chain back to contentDocument.

So you need roll-your-own retry with a recursive function.

Here's a working example using a sample Stripe page:

cy.intercept({ resourceType: /xhr|fetch/ }, { log: false })  // suppress fetch logs 
cy.viewport(1500, 1000)
cy.visit('https://stripe-payments-demo.appspot.com');  

function getCardField(selector, attempts = 0) {
  Cypress.log({displayName: 'getCardField', message: `${selector}: ${attempts}`})
  
  if (attempts > 50) throw new Error('too many attempts')

  return cy.get('iframe', {timeout:10_000, log:false})
    .eq(1, {log:false})
    .its('0.contentDocument', {log:false}) 
    .find('body', {log:false})
    .then(body => {
      const cardField = body.find(selector)
      if (!cardField.length) {
        return cy.wait(300, {log:false})
          .then(() => {
            getCardField(selector, ++attempts)
          })
      } else {
        return cy.wrap(cardField)
      }
    })
}

getCardField('div.CardField')
  .type('4242424242424242')

getCardField('div.CardField')
  .find('input').eq(0)
  .should('have.value', '4242 4242 4242 4242')   // ✅ passes

A more general recursive function

function getStripeField({iframeSelector, fieldSelector}, attempts = 0) {
  Cypress.log({displayName: 'getCardField', message: `${fieldSelector}: ${attempts}`})

  if (attempts > 50) throw new Error('too many attempts')

  return cy.get(iframeSelector, {timeout:10_000, log:false})
    .eq(0, {log:false})
    .its('0.contentDocument', {log:false}) 
    .find('body', {log:false})
    .then(body => {
      const stripeField = body.find(fieldSelector)
      if (!stripeField.length) {
        return cy.wait(300, {log:false})
          .then(() => {
            getStripeField({iframeSelector, fieldSelector}, ++attempts)
          })
      } else {
        return cy.wrap(stripeField)
      }
    })
}
cy.visit('https://hivepass.app/temp-stripe-example.html')

getStripeField({
  iframeSelector: 'iframe[title="Secure card number input frame"]', 
  fieldSelector: 'div.CardNumberField-input-wrapper'
})
.type('4242424242424242')

getStripeField({
  iframeSelector: 'iframe[title="Secure card number input frame"]', 
  fieldSelector: 'div.CardNumberField-input-wrapper input'
})
.should('have.value', '4242 4242 4242 4242')

getStripeField({
  iframeSelector: '[title="Secure expiration date input frame"]', 
  fieldSelector: '[name="exp-date"]'
})
.type('0323')

getStripeField({
  iframeSelector: '[title="Secure expiration date input frame"]', 
  fieldSelector: '[name="exp-date"]'
})
.should('have.value', '03 / 23')

Note, it seems to be important to re-query the stripe field after updating, when confirming it's new value.

Nosology answered 30/3, 2023 at 2:23 Comment(5)
thanks so much for the detailed answer. It works with the Stripe Elements test page but I can't get it to work with my Stripe Elements page. I have put my page up here so you can give it a try. Sorry about the formatting as no styling. hivepass.app/temp-stripe-example.html I am working on this now trying to figure it out.Gwalior
I think I may have it. Currently testing the date, CVC and submission. See the my update above. Thanks for the pointers. Much appreciated.Gwalior
The .eq(1) is specific to the https://stripe-payments-demo.appspot.com page, so that does need changing, but I couldn't get passed the .its('0.contentDocument') line. Instead I used 0.contentWindow.window.document and it seems to work.Nosology
Its now working in the actual app. I am having some timing issues with it getting the incorrect iframe as the iframe I want is not available yet and there is already an iframe on the page. Referring to it by element number is not ideal. For now I have put cy.wait(10000) in front of it and it seems to work. This will be a great resource for future people using Stripe with Cypress as I could find nothing that worked.Gwalior
Your changed worked on my end, I was blocked by it for some reason. I am passing in the iframe selector as well, since each field has a different one. The recursive function should take care of any lag in loading one iframe or another, if you can find a unique selector for each.Nosology
R
1

Here's what worked for me. Try it out.

Cypress.Commands.add("populateCardInformationForm", (creditCardDetails) => {
  cy.log("Going to populateCardInformationForm()");
  // Populate Credit Card number
  const getIframeBody1 = () => {
    return cy
      .get('iframe[name*="privateStripeFrame"]')
      .eq(0)
      .its("0.contentDocument.body")
      .should("not.be.empty")
      .then(cy.wrap);
  };
  getIframeBody1().find('input[class^="InputElement"]').type(creditCardDetails.cardNumber);

  // Populate Expiry date
  const getIframeBody2 = () => {
    return cy
      .get('iframe[name*="privateStripeFrame"]')
      .eq(1)
      .its("0.contentDocument.body")
      .should("not.be.empty")
      .then(cy.wrap);
  };
  getIframeBody2().find('input[class^="InputElement"]').type(creditCardDetails.expiry);

  // Populate CVC number
  const getIframeBody3 = () => {
    return cy
      .get('iframe[name*="privateStripeFrame"]')
      .eq(2)
      .its("0.contentDocument.body")
      .should("not.be.empty")
      .then(cy.wrap);
  };
  getIframeBody3().find('input[class^="InputElement"]').type(creditCardDetails.cvcNumber);
});
Rattray answered 18/8, 2023 at 22:10 Comment(2)
Sadly, it errors with Your subject contained 3 elements.Darindaring
Works in 2024! Here's a working snippet that should help in TS cy.get('iframe') .its('0.contentDocument.body') .should('not.be.empty') .then(cy.wrap) .within(() => { cy.get('input[name="number"]').type(variables.billing.stripe.number) cy.get('input[name="expiry"]').type(variables.billing.stripe.expirationDate) cy.get('input[name="cvc"]').type(variables.billing.stripe.cvv) })Hrutkay

© 2022 - 2024 — McMap. All rights reserved.