Cypress Testing Stripe Elements
3 min read
I was trying to test the Stripe checkout flow in our app using Cypress. If you’ve not come across Cypress, it’s an end to end testing tool that allows you to test your frontend UI by defining actions a user might take. An example might be clicking on an input field of a form, typing something in, then clicking on a button to submit the form. Stripe is a payment processing platform, and the app I’m working on specifically uses Stripe elements, which provides embeddable UI components that allow us to customise our payments form, in line with the rest of our app.
Cypress is normally pretty easy to get up and running with. All you have to do is select an element using a selector, define the action you want to take, and you’re off to the races. However, if the element you want lives within an iframe, it’s not as easy to deal with. This is because you’ve not only got to wait until you’re sure the iframe exists and is loaded, but also deal with the fact that Cypress’s DOM traversal stops whenever it hits the #document
node inside the iframe.
This was something I had to overcome when testing Stripe elements, because the UI components load within an iframe. I tried following the instructions in this Cypress blog post, but unfortunately couldn’t get it to work - Cypress found the iframe, but not the payment card Stripe element field. Upon further inspection of the error, it seemed that the content body of the iframe was not loading in time for Cypress to find.
After a lot of experimentation, I managed to get it working with this setup.
// stripe.spec.ts
const STRIPE_IFRAME_PREFIX = '__privateStripeFrame'
const CARD_DETAILS = {
cardNumber: '4000058260000005',
cardExpiry: '0525',
cardCvc: '123',
}
const getStripeIFrameDocument = () => {
return cy.checkElementExists(`iframe[name^="${STRIPE_IFRAME_PREFIX}"]`).iframeCustom()
}
it('Can enter card payment details', () => {
getStripeIFrameDocument().find('input[data-elements-stable-field-name="cardNumber"]').type(CARD_DETAILS.cardNumber)
getStripeIFrameDocument().find('input[data-elements-stable-field-name="cardExpiry"]').type(CARD_DETAILS.cardExpiry)
getStripeIFrameDocument().find('input[data-elements-stable-field-name="cardCvc"]').type(CARD_DETAILS.cardCvc)
})
I created Cypress custom commands for checkElementExists
and iframeCustom
.
// cypress/support/index.ts
Cypress.Commands.add('iframeCustom', { prevSubject: 'element' }, ($iframe) => {
return new Cypress.Promise((resolve) => {
$iframe.ready(function () {
resolve($iframe.contents().find('body'))
})
})
})
Cypress.Commands.add('checkElementExists', (selector) => {
return cy.get(selector).should('exist').then(cy.wrap)
})
What I essentially did here was to check that the Stripe parent iframe exists (with a name attribute that contains __privateStripeFrame
using jQuery-style selection), before then using Cypress.Promise to resolve the contents of the iframe and find the body
element. This ensures that Cypress keeps trying to find the body
whilst the iframe loads.
From here, I then try to find the various Stripe element fields by the data-elements-stable-field-name
attribute. This is provided by Stripe, and seems consistent and thus safe to use as a selector.