How to simulate loadStripe behavior with Cypress stub?

I am struggling to correctly stub out Stripe from my tests

CartCheckoutButton.ts

import React from 'react'
import { loadStripe } from '@stripe/stripe-js'

import useCart from '~/state/CartContext'
import styles from './CartCheckoutButton.module.scss'

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY)

const CartCheckoutButton = ({}: TCartCheckoutButtonProps) => {
  const { cartItems } = useCart()

  const handleCheckOutOnClick = async (event) => {
    const { sessionId } = await fetch('/api/checkout/session', {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
      },
      body: JSON.stringify({ cartItems }),
    }).then((res) => res.json())

    const stripe = await stripePromise
    const { error } = await stripe.redirectToCheckout({
      sessionId,
    })

    if (error) {
      // TODO: Show some error message
      console.log(error)
    }
  }

  return (
    <div className={styles.container}>
      <button onClick={handleCheckOutOnClick} disabled={cartItems.length == 0}>
        CHECKOUT
      </button>
    </div>
  )
}

export default CartCheckoutButton

EndUserExperience.spec.js

import * as stripeJS from '@stripe/stripe-js'

describe('End user experience', () => {
  beforeEach(() => {
    cy.visit('http://localhost:3000/')

    cy.stub(stripeJS, 'loadStripe').resolves(
      new Promise(function (resolve, reject) {
        resolve({
          redirectToCheckout({ sessionId }) {
            console.log(`redirectToCheckout called with sessionId: ${sessionId}`)
            return new Promise(function (resolve, reject) {
              resolve({ error: true })
            })
          },
        })
      })
    )
  })

  it('Orders some dishes and makes a checkout', () => {
    console.log('working on it')
  })
})

When I click around it still redirects me. So the stub did not seem to kick in..

Update II

Trying out the following solution suggested by @RichardMatsen

import React from 'react'
import * as stripeModule from '@stripe/stripe-js'

import useCart from '~/state/CartContext'
import styles from './CartCheckoutButton.module.scss'

const stripePublishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY

const CartCheckoutButton = ({}: TCartCheckoutButtonProps) => {
  const { cartItems } = useCart()

  // https://stackoverflow.com/questions/67565714/cypress-stub-out-loadstripe
  const stripePromise = React.useCallback(() => {
    window['stripeModule'] = stripeModule
    return stripeModule.loadStripe(stripePublishableKey)
  }, [stripeModule, stripePublishableKey])

  const handleCheckOutOnClick = async (event) => {
    const { sessionId } = await fetch('/api/checkout/session', {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
      },
      body: JSON.stringify({ cartItems }),
    }).then((res) => res.json())

    const stripe = await stripePromise()
    const { error } = await stripe.redirectToCheckout({
      sessionId,
    })

    if (error) {
      // TODO: Show some error message
      console.log(error)
      throw error
    }
  }

  return (
    <div className={styles.container}>
      <button onClick={handleCheckOutOnClick} disabled={cartItems.length == 0}>
        TILL KASSAN
      </button>
    </div>
  )
}

export default CartCheckoutButton

test.spec.js

describe('End user experience', async () => {
  beforeEach(() => {
    cy.visit('http://localhost:3000/')

    cy.window().then((win) => {
      console.log(win)
      cy.stub(win.stripeModule, 'loadStripe').resolves(
        new Promise(function (resolve, reject) {
          resolve({
            redirectToCheckout({ sessionId }) {
              console.log(`redirectToCheckout called with sessionId: ${sessionId}`)
              return new Promise(function (resolve, reject) {
                resolve({ error: true })
              })
            },
          })
        })
      )
    })

    cy.intercept('GET', /.*stripe.*/, (req) => {
      req.redirect('http://localhost:3000/checkout/success')
    })
  })

  it('Orders some dishes and makes a checkout', () => {
    console.log('working on it')
  })
})

But it still redirect me and display an error

Trying to stub property 'loadStripe' of undefined

Answer №1

From my understanding, attempting to stub a method within the app by importing its module in the test does not seem to work as expected and results in a different "instance" being obtained.

For more information on this topic, I recommend checking out this recent question How to Stub a module in Cypress. One approach that has been successful is passing the instance to be stubbed via window.

CartCheckoutButton.ts

import React, { useCallback } from 'react'
import * as stripeModule from '@stripe/stripe-js';

if (process.browser) {
  if (window.Cypress) {
    window.stripeModule = stripeModule;
  }
}

const stripeKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;


const CartCheckoutButton = ({}: TCartCheckoutButtonProps) => {

   const stripePromise = useCallback(() => {
     return stripeModule.loadStripe(stripeKey);
   }, [stripeModule, stripeKey]);

EndUserExperience.spec.js

beforeEach(() => {

  cy.visit('http://localhost:3000/')
    .then(win => {                      
      const stripeModule = win.stripeModule;
      cy.stub(stripeModule, 'loadStripe').resolves(...

    })

Reproducible example

Set up the default Next app

npx create-next-app nextjs-blog --use-npm --example "https://github.com/vercel/next-learn-starter/tree/master/learn-starter"

Add reference to stripeModule and useCallback() in /pages/index.js

import React, { useCallback } from 'react'
import * as stripeModule from '@stripe/stripe-js';

import Head from 'next/head'

if (process.browser) {
  if (window.Cypress) {
    window.stripeModule = stripeModule; 
  }
}

const stripeKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;

export default function Home() {

  const stripePromise = useCallback(() => {
    return stripeModule.loadStripe(stripeKey);
  }, [stripeModule, stripeKey]);

  return (
    <div className="container">
    ...

Add a basic test

it('stubs loadStripe', () => {

  cy.visit('http://localhost:3000/').then(win => {
    const stripeModule = win.stripeModule;
    cy.stub(stripeModule, 'loadStripe').resolves(
      console.log('Hello from stubbed loadStripe')
    )
  })
})

Build, start, test

yarn build
yarn start
yarn cypress open

The message from cy.stub() will be displayed in the console.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Angular JS encountered an issue with executing 'removeChild' on 'Node' for HTMLScriptElement.callback, leading to an uncaught DOMException

I am currently using Angular's JSON HTTP call. When making a post request, I experience an error during runtime: Uncaught TypeError: Cannot read property 'parentElement' of undefined at checklistFunc (masterlowerlibs.67785a6….js:42972 ...

Is npm create-react-app giving you trouble?

When attempting to create a React app using the command npm create-react-app appname, the tool would just return me to the same line to input more code. I also gave npx a try, but encountered some errors in the process. See this screenshot for reference: ...

Press the button to dynamically update the CSS using PHP and AJAX

I have been facing challenges with Ajax, so I decided to switch to using plain JavaScript instead. My goal is to create a button on a banner that will allow me to toggle between two pre-existing CSS files to change the style of the page. I already have a f ...

Exploring the world of Node.js with fs, path, and the

I am facing an issue with the readdirSync function in my application. I need to access a specific folder located at the root of my app. development --allure ----allure-result --myapproot ----myapp.js The folder I want to read is allure-results, and to d ...

Tracking page views through ajax requests on Google Analytics

I have implemented a method to log page views through ajax action when the inner page content is loaded. However, I am facing an issue where the bounce rate data is not getting updated and always shows 0%. The default Google Analytics page view is logged ...

When the Component is mounted, it triggers a GET request and passes the response as a prop to the child component. What occurs if I enter a URL that directly accesses the child

Consider a scenario where there are 2 URLs: website.com/Overview website.com/Overview/City When website.com/Overview is accessed, the component shown is Overview, while accessing website.com/Overview/City reveals the City component. An issue arises when ...

Tips for addressing the error message "Cannot PUT /":

I have been working on updating a local database using MongoDB for my project. Here is the code snippet I am using to update the data. The second part involves editing that redirects the updated data. I am not encountering any errors, so I am unable to i ...

Caution: npm installation warns about potential issues

After encountering some WARN messages, I attempted to update npm by running npm audit fix However, this resulted in even more WARN messages. When I tried to install all dependencies using npm i I was bombarded with a plethora of WARN messages (see below) ...

Find the item that has a specific value in its sub-property

Information Models: public class Header{ public int Identifier; public int value; public bool anotherValue; public List<Trailer> trailers; } public class Trailer{ public int ID; public Language MyLanguage; public string ...

One the year is chosen, it will be automatically hidden and no longer available for selection

<div ng-repeat="localcost in vm.project.localCosts" layout="column"> <md-select name="localcost_{{$index}}"ng-model="localcost.year" flex> <md-option ng-repeat="years in vm.getYears()" ng-value="years">{{years}}< ...

When a previous form field is filled, validate the next 3 form fields on keyup using jQuery

Upon form submission, if the formfield propBacklink has a value, the validation of fields X, Y, and Z must occur. These fields are always validated, regardless of their values, as they are readonly. An Ajax call will determine whether the validation is tru ...

Enhancing nested structures in reducers

Currently, I am utilizing react, typescript, and redux to develop a basic application that helps me manage my ingredients. However, I am facing difficulties with my code implementation. Below is an excerpt of my code: In the file types.ts, I have declared ...

What is the purpose of parsing my array if they appear identical in the console?

While working on a D3 component, I came across an interesting question. Why do different arrays require different data treatment even if they all contain the same information? In the first case, I have a 'hardcoded' array that looks like this: ...

What is the best way to pass an email as a Laravel parameter within a function using Angular?

I'm currently working on a feature that allows users to delete their account only if the input field matches their email address. However, I encountered an error: Error: $parse:lexerr Lexer Error when attempting to set this up. Here is my HTML code: ...

I'm having difficulty implementing a vue-awesome icon on my project

I am attempting to utilize the standard window-close icon from vue-awesome. I have imported my vue-awesome using the following code: import 'vue-awesome/icons'; import Icon from 'vue-awesome/components/Icon.vue'; Vue.component('i ...

How to reference an image located in one folder from a page in a different folder using React

Imagine there is Folder A containing an image called 'image.jpg' and Folder B with an HTML page that needs to access the image in Folder A in order to display it. The following code successfully accomplishes this task: <img src={require(&apo ...

Automatic execution of expressions in browserify upon initialization

Utilizing browserify alongside node.js allows me to utilize require() in my JavaScript files. Within my game.js file, I have the following code: var game = new Phaser.Game(800, 600, Phaser.AUTO, 'snakeGame'); var menuState = require('./me ...

Exported data in Vue3 components cannot be accessed inside the component itself

Essentially, I'm working on creating a dynamic array in Vue3. Each time a button is clicked, the length of the array should increase. Below is the code snippet. <div class="package-item" v-for="n in arraySize"></div> e ...

Combining the elements within an array of integers

Can anyone provide insight on how to sum the contents of an integer array using a for loop? I seem to be stuck with my current logic. Below is the code I've been working on: <p id='para'></p> var someArray = [1,2,3,4,5]; funct ...

What is the best way to modify Mega Menus using JavaScript?

I am currently managing a website that features "mega menu" style menus. These menus consist of nested <UL> elements and contain approximately 150 to 200 entries, resulting in a heavy load on the page and posing challenges for screen readers. To add ...