From 6d4fef221d2507075a1a4f1a33600a86cf30b851 Mon Sep 17 00:00:00 2001 From: wdrussell2015 <43426024+wdrussell2015@users.noreply.github.com> Date: Tue, 31 Jan 2023 11:15:41 -0500 Subject: [PATCH] test: Added Stripe payment form tests (#703) [REV-3039](https://2u-internal.atlassian.net/browse/REV-3039) --- src/payment/checkout/Checkout.test.jsx | 34 ++- .../CardHolderInformation.test.jsx | 143 +++++---- .../payment-form/PaymentForm.test.jsx | 2 +- .../payment-form/StripePaymentForm.test.jsx | 276 ++++++++++++++++++ src/payment/checkout/stripeMocks.js | 29 ++ 5 files changed, 396 insertions(+), 88 deletions(-) create mode 100644 src/payment/checkout/payment-form/StripePaymentForm.test.jsx create mode 100644 src/payment/checkout/stripeMocks.js diff --git a/src/payment/checkout/Checkout.test.jsx b/src/payment/checkout/Checkout.test.jsx index 1c485d945..123fe7122 100644 --- a/src/payment/checkout/Checkout.test.jsx +++ b/src/payment/checkout/Checkout.test.jsx @@ -11,11 +11,14 @@ import { submitPayment } from '../data/actions'; import '../__factories__/basket.factory'; import '../__factories__/userAccount.factory'; import { transformResults } from '../data/service'; +import { getPerformanceProperties } from '../performanceEventing'; jest.mock('@edx/frontend-platform/analytics', () => ({ sendTrackEvent: jest.fn(), })); +jest.useFakeTimers('modern'); + configureI18n({ config: { ENVIRONMENT: process.env.ENVIRONMENT, @@ -106,19 +109,24 @@ describe('', () => { // Apple Pay temporarily disabled per REV-927 - https://github.com/openedx/frontend-app-payment/pull/256 - // TODO: Disabling for now update once we can swap between stripe and cybersource - // it('submits and tracks the payment form', () => { - // const formSubmitButton = wrapper.find('form button[type="submit"]').hostNodes(); - // formSubmitButton.simulate('click'); - - // expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.basket.payment_selected', { - // type: 'click', - // category: 'checkout', - // paymentMethod: 'Credit Card', - // checkoutType: 'client_side', - // flexMicroformEnabled: true, - // }); - // }); + it('submits and tracks the payment form', () => { + expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.payment_mfe.payment_form_rendered', { + ...getPerformanceProperties(), + paymentProcessor: 'Cybersource', + }); + const formSubmitButton = wrapper.find('form button[type="submit"]').hostNodes(); + formSubmitButton.simulate('click'); + + expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.basket.payment_selected', { + type: 'click', + category: 'checkout', + paymentMethod: 'Credit Card', + checkoutType: 'client_side', + flexMicroformEnabled: true, + stripeEnabled: false, + + }); + }); it('fires an action when handling a cybersource submission', () => { const formData = { name: 'test' }; diff --git a/src/payment/checkout/payment-form/CardHolderInformation.test.jsx b/src/payment/checkout/payment-form/CardHolderInformation.test.jsx index d6d9f4a7f..8d2fa84e3 100644 --- a/src/payment/checkout/payment-form/CardHolderInformation.test.jsx +++ b/src/payment/checkout/payment-form/CardHolderInformation.test.jsx @@ -1,21 +1,20 @@ /* eslint-disable react/jsx-no-constructed-context-values */ -// import React from 'react'; -// import { Provider } from 'react-redux'; -// import { mount } from 'enzyme'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { mount } from 'enzyme'; import { - // IntlProvider, + IntlProvider, configure as configureI18n, } from '@edx/frontend-platform/i18n'; -// import { AppContext } from '@edx/frontend-platform/react'; -// import { Factory } from 'rosie'; -// import { createStore } from 'redux'; +import { AppContext } from '@edx/frontend-platform/react'; +import { Factory } from 'rosie'; +import { createStore } from 'redux'; -// import CardHolderInformation, { CardHolderInformationComponent } from './CardHolderInformation'; -// import PaymentForm from './PaymentForm'; -// import createRootReducer from '../../../data/reducers'; +import CardHolderInformation, { CardHolderInformationComponent } from './CardHolderInformation'; +import PaymentForm from './PaymentForm'; +import createRootReducer from '../../../data/reducers'; import '../../__factories__/userAccount.factory'; -// import { iteratee } from 'lodash'; jest.mock('@edx/frontend-platform/analytics', () => ({ sendTrackEvent: jest.fn(), @@ -47,70 +46,66 @@ configureI18n({ }, }); -it('is using stripe', () => { - expect(null).toEqual(null); -}); -// TODO: Disabling for now update once we can swap between stripe and cybersource -// describe('', () => { -// let store; +describe('', () => { + let store; -// describe('handleSelectCountry', () => { -// it('updates state with selected country', () => { -// const authenticatedUser = Factory.build('userAccount'); + describe('handleSelectCountry', () => { + it('updates state with selected country', () => { + const authenticatedUser = Factory.build('userAccount'); -// store = createStore(createRootReducer(), {}); -// const component = ( -// -// -// -// {}} onSubmitButtonClick={() => {}}> -// -// -// -// -// -// ); -// const wrapper = mount(component); -// const cardHolderInformation = wrapper -// .find(CardHolderInformationComponent) -// .first() -// .instance(); -// const eventMock = jest.fn(); + store = createStore(createRootReducer(), {}); + const component = ( + + + + {}} onSubmitButtonClick={() => {}}> + + + + + + ); + const wrapper = mount(component); + const cardHolderInformation = wrapper + .find(CardHolderInformationComponent) + .first() + .instance(); + const eventMock = jest.fn(); -// cardHolderInformation.handleSelectCountry(eventMock, 'US'); + cardHolderInformation.handleSelectCountry(eventMock, 'US'); -// expect(cardHolderInformation.state).toEqual({ selectedCountry: 'US' }); -// }); -// }); -// describe('purchasedForOrganization field', () => { -// it('renders for bulk purchase', () => { -// const wrapper = mount(( -// -// -// {}} -// onSubmitPayment={() => {}} -// onSubmitButtonClick={() => {}} -// /> -// -// -// )); -// expect(wrapper.exists('#purchasedForOrganization')).toEqual(true); -// }); -// it('does not render if not bulk purchase', () => { -// const wrapper = mount(( -// -// -// {}} -// onSubmitPayment={() => {}} -// onSubmitButtonClick={() => {}} -// /> -// -// -// )); -// expect(wrapper.exists('#purchasedForOrganization')).toEqual(false); -// }); -// }); -// }); + expect(cardHolderInformation.state).toEqual({ selectedCountry: 'US' }); + }); + }); + describe('purchasedForOrganization field', () => { + it('renders for bulk purchase', () => { + const wrapper = mount(( + + + {}} + onSubmitPayment={() => {}} + onSubmitButtonClick={() => {}} + /> + + + )); + expect(wrapper.exists('#purchasedForOrganization')).toEqual(true); + }); + it('does not render if not bulk purchase', () => { + const wrapper = mount(( + + + {}} + onSubmitPayment={() => {}} + onSubmitButtonClick={() => {}} + /> + + + )); + expect(wrapper.exists('#purchasedForOrganization')).toEqual(false); + }); + }); +}); diff --git a/src/payment/checkout/payment-form/PaymentForm.test.jsx b/src/payment/checkout/payment-form/PaymentForm.test.jsx index cdee96b66..561474c2b 100644 --- a/src/payment/checkout/payment-form/PaymentForm.test.jsx +++ b/src/payment/checkout/payment-form/PaymentForm.test.jsx @@ -82,7 +82,7 @@ describe('', () => { lastName: '', address: '', city: '', - country: 'UK', + country: 'GB', cardExpirationMonth: '', cardExpirationYear: '', optionalField: '', diff --git a/src/payment/checkout/payment-form/StripePaymentForm.test.jsx b/src/payment/checkout/payment-form/StripePaymentForm.test.jsx new file mode 100644 index 000000000..f2e830506 --- /dev/null +++ b/src/payment/checkout/payment-form/StripePaymentForm.test.jsx @@ -0,0 +1,276 @@ +/* eslint-disable react/jsx-no-constructed-context-values */ +import React from 'react'; +import * as reactRedux from 'react-redux'; +import { createStore } from 'redux'; +import { mount } from 'enzyme'; +import { IntlProvider, configure as configureI18n } from '@edx/frontend-platform/i18n'; +import { Factory } from 'rosie'; +import configureMockStore from 'redux-mock-store'; + +import { Elements, PaymentElement } from '@stripe/react-stripe-js'; +import { AppContext } from '@edx/frontend-platform/react'; +import StripePaymentForm from './StripePaymentForm'; +import * as formValidators from './utils/form-validators'; +import createRootReducer from '../../../data/reducers'; +import '../../__factories__/userAccount.factory'; +import * as mocks from '../stripeMocks'; +import PlaceOrderButton from './PlaceOrderButton'; + +jest.mock('@edx/frontend-platform/analytics', () => ({ + sendTrackEvent: jest.fn(), +})); + +jest.useFakeTimers('modern'); + +const validateRequiredFieldsMock = jest.spyOn(formValidators, 'validateRequiredFields'); + +const mockStore = configureMockStore(); + +configureI18n({ + config: { + ENVIRONMENT: process.env.ENVIRONMENT, + LANGUAGE_PREFERENCE_COOKIE_NAME: process.env.LANGUAGE_PREFERENCE_COOKIE_NAME, + }, + loggingService: { + logError: jest.fn(), + logInfo: jest.fn(), + }, + messages: { + uk: {}, + th: {}, + ru: {}, + 'pt-br': {}, + pl: {}, + 'ko-kr': {}, + id: {}, + he: {}, + ca: {}, + 'zh-cn': {}, + fr: {}, + 'es-419': {}, + ar: {}, + }, +}); + +const authenticatedUser = Factory.build('userAccount'); + +describe('', () => { + let store; + let mockStripe; + let mockElements; + let submitButton; + let state; + let mockElement; + let paymentElement; + + beforeEach(() => { + store = createStore(createRootReducer(), {}); + mockStripe = mocks.mockStripe(); + mockElement = mocks.mockElement(); + mockElements = mocks.mockElements(); + mockStripe.elements.mockReturnValue(mockElements); + mockElements.create.mockReturnValue(mockElement); + + state = { + payment: { + basket: { + loading: true, + loaded: false, + submitting: false, + redirect: false, + isBasketProcessing: false, + products: [{ sku: '00000' }], + enableStripePaymentProcessor: true, + }, + clientSecret: { isClientSecretProcessing: false, clientSecretId: '' }, + }, + feedback: { byId: {}, orderedIds: [] }, + form: {}, + }; + + const wrapper = mount(( + + + + + {}} + onSubmitPayment={() => {}} + onSubmitButtonClick={() => {}} + /> + + + + + )); + paymentElement = wrapper.find(PaymentElement); + submitButton = wrapper.find(PlaceOrderButton); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('load stripe', () => { + it('creates the payment element', () => { + expect(mockElements.create).toHaveBeenCalledWith('payment', null); + expect(paymentElement.length > 0).toBe(true); + }); + }); + + describe('getRequiredFields', () => { + it('returns expected required fields', () => { + const testFormValues = [ + { + firstName: '', + lastName: '', + address: '', + city: '', + country: 'GB', + postalCode: '', + optionalField: '', + }, + { + firstName: '', + lastName: '', + address: '', + city: '', + country: 'US', + postalCode: '', + state: '', + optionalField: '', + }, + { + firstName: '', + lastName: '', + address: '', + city: '', + country: 'IN', + state: '', + optionalField: '', + }, + ]; + + testFormValues.forEach((formValues) => { + const requiredFields = formValidators.getRequiredFields(formValues, false, true); + const { optionalField, ...expectedRequiredFields } = formValues; + expect(requiredFields).toStrictEqual(expectedRequiredFields); + }); + }); + + it('returns organization fields for a bulk order', () => { + const isBulkOrder = true; + mount(( + + + + + {}} + onSubmitPayment={() => {}} + onSubmitButtonClick={() => {}} + /> + + + + + )); + + const formValues = { + firstName: '', + lastName: '', + address: '', + city: '', + country: 'UK', + cardNumber: '', + securityCode: '', + cardExpirationMonth: '', + cardExpirationYear: '', + optionalField: '', + organization: 'edx', + }; + + const requiredFields = formValidators.getRequiredFields(formValues, isBulkOrder); + expect(formValues.organization).toEqual(requiredFields.organization); + }); + }); + describe('onSubmit', () => { + it('throws expected errors', () => { + state = { + payment: { + basket: { + loading: true, + loaded: false, + submitting: false, + redirect: false, + isBasketProcessing: false, + products: [{ sku: '00000' }], + enableStripePaymentProcessor: true, + }, + clientSecret: { isClientSecretProcessing: false, clientSecretId: '' }, + }, + feedback: { byId: {}, orderedIds: [] }, + form: { + payment: { + submitErrors: { firstName: 'error' }, + }, + }, + }; + const submitStripePayment = jest.fn(); + mount(( + + + + + {}} + onSubmitPayment={() => submitStripePayment} + onSubmitButtonClick={() => {}} + shouldFocusFirstError + firstErrorId={null} + /> + + + + + )); + const SubmissionError = jest.fn(); + const testData = [ + [ + { firstName: 'This field is required' }, + new SubmissionError({ + firstName: 'This field is required', + }), + ], + [ + {}, + null, + ], + ]; + + testData.forEach((testCaseData) => { + validateRequiredFieldsMock.mockReturnValueOnce(testCaseData[0]); + if (testCaseData[1]) { + expect(() => submitButton.simulate('click')); + expect(submitStripePayment).not.toHaveBeenCalled(); + } else { + expect(() => submitButton.simulate('click')).not.toThrow(); + } + }); + }); + }); + + describe('validateRequiredFields', () => { + it('returns errors if values are empty', () => { + const values = { + firsName: 'Jane', + lastName: undefined, + }; + const expectedErrors = { + lastName: 'payment.form.errors.required.field', + }; + expect(formValidators.validateRequiredFields(values)).toEqual(expectedErrors); + }); + }); +}); diff --git a/src/payment/checkout/stripeMocks.js b/src/payment/checkout/stripeMocks.js new file mode 100644 index 000000000..90f0202f0 --- /dev/null +++ b/src/payment/checkout/stripeMocks.js @@ -0,0 +1,29 @@ +export const mockElement = () => ({ + mount: jest.fn(), + destroy: jest.fn(), + on: jest.fn(), + update: jest.fn(), +}); + +export const mockElements = () => { + const elements = {}; + return { + create: jest.fn((type) => { + elements[type] = mockElement(); + return elements[type]; + }), + getElement: jest.fn((type) => elements[type] || null), + }; +}; + +export const mockStripe = () => ({ + elements: jest.fn(() => mockElements()), + createToken: jest.fn(), + createSource: jest.fn(), + createPaymentMethod: jest.fn(), + confirmCardPayment: jest.fn(), + confirmCardSetup: jest.fn(), + paymentRequest: jest.fn(), + registerAppInfo: jest.fn(), + _registerWrapper: jest.fn(), +});