dbalatero / cypress-plugin-stripe-elements

A small Cypress plugin that assists you in filling in Stripe Elements inputs
MIT License
37 stars 5 forks source link

Handling SCA/3DS #6

Open mike-mccormick opened 3 years ago

mike-mccormick commented 3 years ago

Hey,

Firstly, this plugin's fantastic, and saves a ton of time, thanks!

I'm wondering if anyone has any process worked out for handling the 3DS iFrame/Modal styled popup when using test cards like 4000002760003184.

image

Even in element inspector in Chrome, I don't feel like the contents of the frame's really being surfaced in a way you'd be able to interact with (short of using a fixed window size, and using x & y to click the screen)..

This would be very useful to me for testing transaction state in applications under test, as handling drop-offs during SCA approval is it's own challenge.

Thanks!

dbalatero commented 3 years ago

Share a failing test with me here so I can reproduce this? Otherwise it'll be high friction for me to look into it. Thanks!

mike-mccormick commented 3 years ago

Hey @dbalatero, sorry should have gotten something set up.

I'm using cypress-cucumber-preprocessor here too, as well as but this is the demo test I threw together to test your plugin.

If you swap the card number in the line: cy.enterStripeDetails('4000002500003155', '12/23', 123) over to the standard 4242... one, it works great, I just can't find any way to get inside that complete/fail modal!

Thanks for your time, and let me know if there's anything else you need from me!

cypress/stripe.feature


Feature: Checkout

  Scenario: Pay through Stripe
    Given I visit the stripe demo page
    And I enter the standardContact contact information
    And I enter card details
    And I click the Pay button
    Then The success message will appear on screen

cypress/integration/stripe/stripe.js

import {And, Given, Then} from "cypress-cucumber-preprocessor/steps";
import 'cypress-plugin-stripe-elements';

const testUsers = require("../../fixtures/testUsers")

Given('I visit the stripe demo page', function() {
   cy.visit('https://stripe-payments-demo.appspot.com/');
});

And('I enter the {} contact information', function(record) {

   //Name
   cy.get("input[name='name']")
       .type(`${testUsers[record].forename} ${testUsers[record].surname}`)

   //Email
   cy.get("input[name='email']")
       .type(testUsers[record].email)

   //Address
   cy.get("input[name='address']")
       .type(testUsers[record].address1)

   //City
   cy.get("input[name='city']")
       .type(testUsers[record].city)

   //State
   cy.get("input[name='state']")
       //This uses a 'Force' because on the DOM, technically the 'ZIP' field
       //Appears over the top, even though it doesn't visually.
       .type(testUsers[record].state,  {force: true})

   //Zip
   cy.get("input[name='postal_code']")
       .type(testUsers[record].state)

})

Then('I enter card details', function() {
   cy.enterStripeDetails('4000002500003155', '12/23', 123)
})

And(`I click the {} button`, function(buttonText) {
   cy.clickButtonWithText(buttonText)
})

Then(`The success message will appear on screen`, function() {

   //Ensure the success message is visible on the page
   cy.get('.success h1')
       .should('be.visible', {timeout: 10000})

   //Assert the success message is correct
   cy.get('.success h1')
       .should('contain', 'Thanks for your order', {timeout: 10000})
})

cypress/support/stripe.js


//An example of a custom command.
Cypress.Commands.add("enterStripeDetails", (cardNumber, cardExpriy, cardCVV) => {

    cy.fillElementsInput('cardNumber', cardNumber);
    cy.fillElementsInput('cardExpiry', cardExpriy);
    cy.fillElementsInput('cardCvc', cardCVV);

})

cypress/fixtures/testUsers.js

I know this isn't how the Cypress docs show fixtures being used, I just find this cleaner :)


module.exports = {
  "standardContact": {
    "forename": "Standard",
    "surname": "Contact",
    "address1": "123 Fake Street",
    "city": "Spring Field",
    "state": "OH",
    "zip": 12345,
    "email": "test@example.com"
  },
  "contactWithInvalidEmailAddress": {
      "forename": "Invalid",
      "surname": "EmailAddress",
      "address1": "123 Fake Street",
      "city": "Spring Field",
      "state": "OH",
      "zip": 12345,
      "email": "test@@example.com"
  }
dbalatero commented 3 years ago

Hmm, actually – can I ask you to make a focused test inside this repo on a development branch? You can start a PR for this.

https://github.com/dbalatero/cypress-plugin-stripe-elements/blob/master/cypress/integration/basic_spec.ts

Then we can work together to add something to assist with the 3DS modal to this library, once we have a failing test.

Setting up tests in this repo should be fast, follow this short readme.

mike-mccormick commented 3 years ago

Your demo app doesn't seem to support SCA, so both SCA and Non-SCA cards react the same, I think it needs set up to handle paymentIntents and Charges before the SCA challenge process has a change to jump in.

I did however spend a bunch more time trying to get into the iFrame to click the button, and managed to get cypress inside the frame!

Code

Stripe's SCA test modal is three iFrames deep. On the top level is __privateStripeFrameXXXX where XXXX is a randomly generated numbers for that session (hence the wildcard selector), so we have to grab that, then scope into it.

Within that is __stripeJSChallengeFrame, which we also have to scope into.

Finally, grab acsFrame and scope into that, where the buttons live.


    //Find the first frame - Named differently each load ( __privateStripeFrameXXXX )
    cy.get("iframe[name*='__privateStripeFrame']")
        .within(($element) => {

            //Get the body from the first frame
            const $body = $element.contents().find("body");
            let topLevel = cy.wrap($body)

            //Find the second frame
            topLevel.find("iframe[name*='__stripeJSChallengeFrame']")
                .within(($secondElement) => {

                    //Get the body from the second frame
                    const $secondBody = $secondElement.contents().find("body");
                    let secondLevel = cy.wrap($secondBody)

                    //Find the third frame -  acsFrame
                    secondLevel.find("iframe[name*='acsFrame']")

                        //Scope into the actual modal
                        .within(($thirdElement) => {

                            //Get the body of the modal
                            const $actualModal = $thirdElement.contents().find("body");

                            //Find, and click the 'Complete Authentication' Button
                            cy.wrap($actualModal)
                                .find('button')
                                .contains('Complete authentication')
                                .click()
                        })

                })

        })

There's no need to find iFrames by name after the top level one, and this can be refactored down a whole bunch, but I think the raw and messy version shows the process better

Update: It seems there is more than one type of SCA confirm/deny test screen on Stripe, with different button labels (I'm guessing these run through actual card processors?) - this code only works on 4000002500003155

mike-mccormick commented 3 years ago

Inspiration struck me the other day while implementing Puppeteer to handle azure login on another project.. we can actually dig down, grab the source URL of the iFrame and have puppeteer browse to it!

I've found this really reliable so far, so hopefully it helps someone else: Link to gist

estefafdez commented 3 years ago

+1 to this issue, it would be great to have support on this on the plugging!