Skip to content

Commit

Permalink
feat(payment): CHECKOUT-4223 Add payment form component
Browse files Browse the repository at this point in the history
  • Loading branch information
davidchin committed Aug 9, 2019
1 parent f21f9eb commit 34fd528
Show file tree
Hide file tree
Showing 13 changed files with 1,834 additions and 1 deletion.
464 changes: 464 additions & 0 deletions src/app/payment/Payment.spec.tsx

Large diffs are not rendered by default.

437 changes: 437 additions & 0 deletions src/app/payment/Payment.tsx

Large diffs are not rendered by default.

250 changes: 250 additions & 0 deletions src/app/payment/PaymentForm.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import { createCheckoutService, CheckoutSelectors, CheckoutService } from '@bigcommerce/checkout-sdk';
import { mount, ReactWrapper } from 'enzyme';
import React, { FunctionComponent } from 'react';

import { getCart } from '../cart/carts.mock';
import { CheckoutProvider } from '../checkout';
import { getStoreConfig } from '../config/config.mock';
import { getCustomer } from '../customer/customers.mock';
import { createLocaleContext, LocaleContext, LocaleContextType } from '../locale';

import { getCreditCardValidationSchema } from './creditCard';
import { getPaymentMethod } from './payment-methods.mock';
import { PaymentMethodList, PaymentMethodListProps } from './paymentMethod';
import PaymentContext, { PaymentContextProps } from './PaymentContext';
import PaymentForm, { PaymentFormProps } from './PaymentForm';
import SpamProtectionField, { SpamProtectionProps } from './SpamProtectionField';
import StoreCreditField, { StoreCreditFieldProps } from './StoreCreditField';
import StoreCreditOverlay from './StoreCreditOverlay';
import TermsConditionsField, { TermsConditionsFieldProps, TermsConditionsType } from './TermsConditionsField';

describe('PaymentForm', () => {
let checkoutService: CheckoutService;
let checkoutState: CheckoutSelectors;
let defaultProps: PaymentFormProps;
let localeContext: LocaleContextType;
let paymentContext: PaymentContextProps;
let PaymentFormTest: FunctionComponent<PaymentFormProps>;

beforeEach(() => {
defaultProps = {
defaultMethodId: getPaymentMethod().id,
isPaymentDataRequired: jest.fn(() => true),
methods: [getPaymentMethod()],
onSubmit: jest.fn(),
};

checkoutService = createCheckoutService();
checkoutState = checkoutService.getState();
localeContext = createLocaleContext(getStoreConfig());
paymentContext = {
disableSubmit: jest.fn(),
setSubmit: jest.fn(),
setValidationSchema: jest.fn(),
};

jest.spyOn(checkoutService, 'initializePayment')
.mockResolvedValue(checkoutState);

jest.spyOn(checkoutState.data, 'getCart')
.mockReturnValue(getCart());

jest.spyOn(checkoutState.data, 'getConfig')
.mockReturnValue(getStoreConfig());

jest.spyOn(checkoutState.data, 'getCustomer')
.mockReturnValue(getCustomer());

PaymentFormTest = props => (
<CheckoutProvider checkoutService={ checkoutService }>
<PaymentContext.Provider value={ paymentContext }>
<LocaleContext.Provider value={ localeContext }>
<PaymentForm { ...props } />
</LocaleContext.Provider>
</PaymentContext.Provider>
</CheckoutProvider>
);
});

it('renders list of payment methods', () => {
const container = mount(<PaymentFormTest { ...defaultProps } />);
const methodList: ReactWrapper<PaymentMethodListProps> = container.find(PaymentMethodList);

expect(methodList)
.toHaveLength(1);

expect(methodList.prop('methods'))
.toEqual(defaultProps.methods);
});

it('renders terms and conditions field if copy is provided', () => {
const container = mount(<PaymentFormTest
{ ...defaultProps }
termsConditionsText="Accept terms"
isTermsConditionsRequired={ true }
/>);
const termsField: ReactWrapper<TermsConditionsFieldProps> = container.find(TermsConditionsField);

expect(termsField)
.toHaveLength(1);

expect(termsField.props())
.toEqual(expect.objectContaining({
terms: 'Accept terms',
type: TermsConditionsType.TextArea,
}));
});

it('renders terms and conditions field if terms URL is provided', () => {
const container = mount(<PaymentFormTest
{ ...defaultProps }
termsConditionsUrl="https://foobar.com/terms"
isTermsConditionsRequired={ true }
/>);
const termsField: ReactWrapper<TermsConditionsFieldProps> = container.find(TermsConditionsField);

expect(termsField)
.toHaveLength(1);

expect(termsField.props())
.toEqual(expect.objectContaining({
url: 'https://foobar.com/terms',
type: TermsConditionsType.Link,
}));
});

it('does not render terms and conditions field if it is not required', () => {
const container = mount(<PaymentFormTest { ...defaultProps } />);

expect(container.find(TermsConditionsField))
.toHaveLength(0);
});

it('renders spam protection field', () => {
const container = mount(<PaymentFormTest
{ ...defaultProps }
isSpamProtectionEnabled={ true }
/>);
const spamProtectionField: ReactWrapper<SpamProtectionProps> = container.find(SpamProtectionField);

expect(spamProtectionField)
.toHaveLength(1);
});

it('renders store credit field if store credit can be applied', () => {
const container = mount(<PaymentFormTest
{ ...defaultProps }
usableStoreCredit={ 100 }
/>);
const storeCreditField: ReactWrapper<StoreCreditFieldProps> = container.find(StoreCreditField);

expect(storeCreditField)
.toHaveLength(1);

expect(storeCreditField.props())
.toEqual(expect.objectContaining({
name: 'useStoreCredit',
usableStoreCredit: 100,
}));
});

it('does not render store credit field if store credit cannot be applied', () => {
const container = mount(<PaymentFormTest { ...defaultProps } />);

expect(container.find(StoreCreditField))
.toHaveLength(0);
});

it('shows overlay if store credit can cover total cost of order', () => {
jest.spyOn(defaultProps, 'isPaymentDataRequired')
.mockReturnValue(false);

const container = mount(<PaymentFormTest
{ ...defaultProps }
usableStoreCredit={ 1000000 }
/>);

expect(container.find(StoreCreditOverlay))
.toHaveLength(1);
});

it('does not show overlay if store credit cannot cover total cost of order', () => {
const container = mount(<PaymentFormTest
{ ...defaultProps }
usableStoreCredit={ 1 }
/>);

expect(container.find(StoreCreditOverlay))
.toHaveLength(0);
});

it('notifies parent when user selects new payment method', () => {
const handleSelect = jest.fn();
const container = mount(<PaymentFormTest
{ ...defaultProps }
onMethodSelect={ handleSelect }
/>);
const methodList: ReactWrapper<PaymentMethodListProps> = container.find(PaymentMethodList);

// tslint:disable-next-line:no-non-null-assertion
methodList.prop('onSelect')!(defaultProps.methods[0]);

expect(handleSelect)
.toHaveBeenCalled();
});

it('passes form values to parent component', async () => {
const handleSubmit = jest.fn();
const container = mount(<PaymentFormTest
{ ...defaultProps }
onSubmit={ handleSubmit }
/>);

container.find('input[name="ccNumber"]')
.simulate('change', { target: { value: '4111 1111 1111 1111', name: 'ccNumber' } });
container.find('input[name="ccCvv"]')
.simulate('change', { target: { value: '123', name: 'ccCvv' } });
container.find('input[name="ccName"]')
.simulate('change', { target: { value: 'Foo Bar', name: 'ccName' } });
container.find('input[name="ccExpiry"]')
.simulate('change', { target: { value: '10 / 22', name: 'ccExpiry' } });
container.find('form')
.simulate('submit');

await new Promise(resolve => process.nextTick(resolve));

expect(handleSubmit)
.toHaveBeenCalledWith({
ccNumber: '4111 1111 1111 1111',
ccCvv: '123',
ccName: 'Foo Bar',
ccExpiry: '10 / 22',
paymentProviderRadio: defaultProps.defaultMethodId,
shouldSaveInstrument: false,
});
});

it('does not pass form values to parent component if validation fails', async () => {
const handleSubmit = jest.fn();
const container = mount(<PaymentFormTest
{ ...defaultProps }
validationSchema={ getCreditCardValidationSchema({
isCardCodeRequired: true,
language: localeContext.language,
}) }
onSubmit={ handleSubmit }
/>);

container.find('input[name="ccNumber"]')
.simulate('change', { target: { value: '4111', name: 'ccNumber' } });
container.find('input[name="ccExpiry"]')
.simulate('change', { target: { value: '10 / 22', name: 'ccExpiry' } });
container.find('form')
.simulate('submit');

await new Promise(resolve => process.nextTick(resolve));

expect(handleSubmit)
.not.toHaveBeenCalled();
});
});
Loading

0 comments on commit 34fd528

Please sign in to comment.