From bda9f17aec00fc3d9c6487ff608725cf73debfb6 Mon Sep 17 00:00:00 2001 From: nayanavishwa Date: Mon, 2 Mar 2026 13:42:54 -0800 Subject: [PATCH] W-20911530: added unit tests for payments related files. --- .../app/pages/checkout/index.test.js | 92 +++ .../pages/checkout/partials/payment.test.js | 607 ++++++++++++++++++ 2 files changed, 699 insertions(+) create mode 100644 packages/template-retail-react-app/app/pages/checkout/partials/payment.test.js diff --git a/packages/template-retail-react-app/app/pages/checkout/index.test.js b/packages/template-retail-react-app/app/pages/checkout/index.test.js index 34852669cb..21ab8e0428 100644 --- a/packages/template-retail-react-app/app/pages/checkout/index.test.js +++ b/packages/template-retail-react-app/app/pages/checkout/index.test.js @@ -1291,3 +1291,95 @@ describe('Salesforce Payments Integration', () => { ) }) }) + +describe('Checkout error display and submitOrder', () => { + test('place order calls create order and shows Place Order button (non-SF Payments)', async () => { + let currentBasket = JSON.parse(JSON.stringify(scapiBasketWithItem)) + currentBasket.shipments[0].shippingMethod = defaultShippingMethod + currentBasket.customerInfo.email = 'customer@test.com' + currentBasket.shipments[0].shippingAddress = { + address1: '123 Main St', + city: 'Tampa', + countryCode: 'US', + firstName: 'Test', + fullName: 'Test McTester', + id: 'addr1', + lastName: 'McTester', + phone: '(727) 555-1234', + postalCode: '33712', + stateCode: 'FL' + } + currentBasket.billingAddress = currentBasket.shipments[0].shippingAddress + currentBasket.paymentInstruments = [ + { + amount: 0, + paymentCard: {cardType: 'Visa', numberLastDigits: '1111'}, + paymentInstrumentId: 'pi1', + paymentMethodId: 'CREDIT_CARD' + } + ] + + let orderPostCalled = false + global.server.use( + rest.post('*/orders', (req, res, ctx) => { + orderPostCalled = true + return res( + ctx.json({ + ...currentBasket, + ...scapiOrderResponse, + status: 'created' + }) + ) + }), + rest.get('*/baskets', (req, res, ctx) => { + return res(ctx.json({baskets: [currentBasket], total: 1})) + }) + ) + + window.history.pushState({}, 'Checkout', createPathWithDefaults('/checkout')) + const {user} = renderWithProviders(, { + wrapperProps: { + bypassAuth: true, + isGuest: false, + siteAlias: 'uk', + locale: {id: 'en-GB'}, + appConfig: mockConfig.app + } + }) + + await waitFor(() => { + expect(screen.getByTestId('sf-checkout-container')).toBeInTheDocument() + }) + + const placeOrderBtn = await screen.findByTestId('sf-checkout-place-order-btn') + await user.click(placeOrderBtn) + + await waitFor(() => { + expect(orderPostCalled).toBe(true) + }) + expect( + screen.queryByText(/An unexpected error occurred during checkout/i) + ).not.toBeInTheDocument() + }) +}) + +describe('CheckoutContainer with basket and modal', () => { + test('renders checkout with Order Summary and basket productItems for modal', async () => { + window.history.pushState({}, 'Checkout', createPathWithDefaults('/checkout')) + renderWithProviders(, { + wrapperProps: { + bypassAuth: true, + isGuest: false, + siteAlias: 'uk', + locale: {id: 'en-GB'}, + appConfig: mockConfig.app + } + }) + + await waitFor(() => { + expect(screen.getByTestId('sf-checkout-container')).toBeInTheDocument() + }) + + expect(screen.getByTestId('sf-order-summary')).toBeInTheDocument() + }) +}) diff --git a/packages/template-retail-react-app/app/pages/checkout/partials/payment.test.js b/packages/template-retail-react-app/app/pages/checkout/partials/payment.test.js new file mode 100644 index 0000000000..c30ce1ada0 --- /dev/null +++ b/packages/template-retail-react-app/app/pages/checkout/partials/payment.test.js @@ -0,0 +1,607 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import React from 'react' +import {screen, waitFor} from '@testing-library/react' +import Payment from '@salesforce/retail-react-app/app/pages/checkout/partials/payment' +import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils' + +const STEPS = { + CONTACT_INFO: 0, + PICKUP_ADDRESS: 1, + SHIPPING_ADDRESS: 2, + SHIPPING_OPTIONS: 3, + PAYMENT: 4, + REVIEW_ORDER: 5 +} + +const mockGoToStep = jest.fn() +const mockGoToNextStep = jest.fn() +const mockAddPaymentInstrument = jest.fn() +const mockUpdateBillingAddress = jest.fn() +const mockRemovePaymentInstrument = jest.fn() +const mockShowToast = jest.fn() + +const mockUseCheckout = jest.fn(() => ({ + step: STEPS.PAYMENT, + STEPS, + goToStep: mockGoToStep, + goToNextStep: mockGoToNextStep +})) +jest.mock('@salesforce/retail-react-app/app/pages/checkout/util/checkout-context', () => ({ + useCheckout: (...args) => mockUseCheckout(...args) +})) + +const defaultBasketReturn = {data: null, derivedData: {totalItems: 0}} +const mockUseCurrentBasket = jest.fn(() => defaultBasketReturn) +jest.mock('@salesforce/retail-react-app/app/hooks/use-current-basket', () => ({ + useCurrentBasket: () => mockUseCurrentBasket() +})) + +jest.mock('@salesforce/retail-react-app/app/hooks/use-toast', () => ({ + useToast: () => mockShowToast +})) + +jest.mock('@salesforce/commerce-sdk-react', () => { + const actual = jest.requireActual('@salesforce/commerce-sdk-react') + return { + ...actual, + useShopperBasketsV2Mutation: (method) => { + const mocks = { + addPaymentInstrumentToBasket: mockAddPaymentInstrument, + updateBillingAddressForBasket: mockUpdateBillingAddress, + removePaymentInstrumentFromBasket: mockRemovePaymentInstrument + } + return {mutateAsync: mocks[method] || jest.fn().mockResolvedValue({})} + } + } +}) + +jest.mock('@salesforce/retail-react-app/app/components/promo-code', () => ({ + usePromoCode: () => ({}), + PromoCode: () => null +})) + +jest.mock('@salesforce/retail-react-app/app/pages/checkout/partials/payment-form', () => { + // eslint-disable-next-line react/prop-types + function MockPaymentForm({onSubmit}) { + return ( +
{ + e.preventDefault() + onSubmit({ + holder: 'Test Holder', + number: '4111111111111111', + expiry: '12/28', + cardType: 'visa' + }) + }} + > + +
+ ) + } + return {__esModule: true, default: MockPaymentForm} +}) + +jest.mock( + '@salesforce/retail-react-app/app/pages/checkout/partials/shipping-address-selection', + () => { + /* eslint-disable react/prop-types -- form is react-hook-form instance in test mock */ + function MockShippingAddressSelection({form}) { + return ( +
+ +
+ ) + } + /* eslint-enable react/prop-types */ + return {__esModule: true, default: MockShippingAddressSelection} + } +) + +jest.mock('@salesforce/retail-react-app/app/components/address-display', () => ({ + __esModule: true, + default: ({address}) => ( +
+ {address?.address1}, {address?.city}, {address?.postalCode} +
+ ) +})) + +const setUseCurrentBasketData = (basket) => { + mockUseCurrentBasket.mockReturnValue({ + data: basket, + derivedData: {totalItems: basket?.productItems?.length ?? 0} + }) +} + +describe('Payment', () => { + beforeEach(() => { + jest.clearAllMocks() + mockUseCheckout.mockReturnValue({ + step: STEPS.PAYMENT, + STEPS, + goToStep: mockGoToStep, + goToNextStep: mockGoToNextStep + }) + mockUseCurrentBasket.mockReturnValue(defaultBasketReturn) + mockAddPaymentInstrument.mockResolvedValue({}) + mockUpdateBillingAddress.mockResolvedValue({basketId: 'basket-1'}) + mockRemovePaymentInstrument.mockResolvedValue({}) + }) + + describe('rendering', () => { + test('renders Payment heading and Edit Payment Info when step is not PAYMENT', () => { + mockUseCheckout.mockReturnValue({ + step: STEPS.REVIEW_ORDER, + STEPS, + goToStep: mockGoToStep, + goToNextStep: mockGoToNextStep + }) + + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [{shipmentId: 'me', shippingMethod: {c_storePickupEnabled: false}}], + billingAddress: {}, + paymentInstruments: [ + { + paymentInstrumentId: 'pi1', + paymentCard: {cardType: 'Visa', numberLastDigits: '1111'} + } + ] + }) + + renderWithProviders() + expect(screen.getByRole('heading', {name: 'Payment'})).toBeInTheDocument() + expect(screen.getByRole('button', {name: 'Edit Payment Info'})).toBeInTheDocument() + }) + + test('renders PaymentForm when no payment instrument applied', () => { + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [ + { + shipmentId: 'me', + shippingMethod: {c_storePickupEnabled: false}, + shippingAddress: { + address1: '123 Main St', + city: 'Tampa', + countryCode: 'US', + firstName: 'Test', + lastName: 'User', + postalCode: '33712', + stateCode: 'FL' + } + } + ], + billingAddress: null, + paymentInstruments: [] + }) + + renderWithProviders() + expect(screen.getByRole('button', {name: 'Review Order'})).toBeInTheDocument() + }) + + test('renders Credit Card summary and Remove button when payment instrument applied', () => { + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [ + { + shipmentId: 'me', + shippingMethod: {c_storePickupEnabled: false}, + shippingAddress: {address1: '123 Main St', city: 'Tampa', countryCode: 'US'} + } + ], + billingAddress: {address1: '123 Main St', city: 'Tampa', countryCode: 'US'}, + paymentInstruments: [ + { + paymentInstrumentId: 'pi1', + paymentCard: { + cardType: 'Visa', + numberLastDigits: '1111', + expirationMonth: 12, + expirationYear: 2028 + } + } + ] + }) + + renderWithProviders() + expect(screen.getByText('Credit Card')).toBeInTheDocument() + expect(screen.getByText('Visa')).toBeInTheDocument() + expect(screen.getByText(/1111/)).toBeInTheDocument() + expect(screen.getByRole('button', {name: 'Remove'})).toBeInTheDocument() + }) + + test('renders Billing Address section', () => { + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [{shipmentId: 'me', shippingMethod: {c_storePickupEnabled: false}}], + billingAddress: null, + paymentInstruments: [] + }) + + renderWithProviders() + expect(screen.getByText('Billing Address')).toBeInTheDocument() + }) + + test('renders Same as shipping address checkbox when not pickup only', () => { + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [ + { + shipmentId: 'me', + shippingMethod: {c_storePickupEnabled: false}, + shippingAddress: {address1: '123 Main St', city: 'Tampa', countryCode: 'US'} + } + ], + billingAddress: null, + paymentInstruments: [] + }) + + renderWithProviders() + expect(screen.getByText('Same as shipping address')).toBeInTheDocument() + }) + + test('does not render Same as shipping address checkbox when pickup only', () => { + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [ + { + shipmentId: 'me', + shippingMethod: {c_storePickupEnabled: true}, + shippingAddress: null + } + ], + billingAddress: null, + paymentInstruments: [] + }) + + renderWithProviders() + expect(screen.queryByText('Same as shipping address')).not.toBeInTheDocument() + }) + + test('renders Review Order button when editing', () => { + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [{shipmentId: 'me', shippingMethod: {c_storePickupEnabled: false}}], + billingAddress: null, + paymentInstruments: [] + }) + + renderWithProviders() + expect(screen.getByRole('button', {name: 'Review Order'})).toBeInTheDocument() + }) + + test('renders shipping address when billing same as shipping', () => { + const shippingAddress = { + address1: '123 Main St', + city: 'Tampa', + countryCode: 'US', + firstName: 'Test', + lastName: 'User', + postalCode: '33712', + stateCode: 'FL' + } + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [ + { + shipmentId: 'me', + shippingMethod: {c_storePickupEnabled: false}, + shippingAddress + } + ], + billingAddress: null, + paymentInstruments: [] + }) + + renderWithProviders() + expect(screen.getByText(/123 Main St/)).toBeInTheDocument() + expect(screen.getByText(/Tampa/)).toBeInTheDocument() + }) + }) + + describe('Edit Payment Info', () => { + test('calls goToStep with STEPS.PAYMENT when Edit Payment Info is clicked', async () => { + mockUseCheckout.mockReturnValue({ + step: STEPS.REVIEW_ORDER, + STEPS, + goToStep: mockGoToStep, + goToNextStep: mockGoToNextStep + }) + + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [{shipmentId: 'me', shippingMethod: {c_storePickupEnabled: false}}], + billingAddress: {}, + paymentInstruments: [{paymentInstrumentId: 'pi1', paymentCard: {}}] + }) + + const {user} = renderWithProviders() + await user.click(screen.getByRole('button', {name: 'Edit Payment Info'})) + + expect(mockGoToStep).toHaveBeenCalledWith(STEPS.PAYMENT) + }) + }) + + describe('payment submission', () => { + test('calls addPaymentInstrumentToBasket when submitting payment form and no applied payment', async () => { + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [ + { + shipmentId: 'me', + shippingMethod: {c_storePickupEnabled: false}, + shippingAddress: { + address1: '123 Main St', + city: 'Tampa', + countryCode: 'US', + firstName: 'Test', + lastName: 'User', + postalCode: '33712', + stateCode: 'FL' + } + } + ], + billingAddress: { + address1: '123 Main St', + city: 'Tampa', + countryCode: 'US', + firstName: 'Test', + lastName: 'User', + postalCode: '33712', + stateCode: 'FL' + }, + paymentInstruments: [] + }) + + const {user} = renderWithProviders() + await user.click(screen.getByRole('button', {name: 'Submit payment'})) + + await waitFor(() => { + expect(mockAddPaymentInstrument).toHaveBeenCalledWith({ + parameters: {basketId: 'basket-1'}, + body: expect.objectContaining({ + paymentMethodId: 'CREDIT_CARD', + paymentCard: expect.objectContaining({ + holder: 'Test Holder', + cardType: 'Visa', + expirationMonth: 12, + expirationYear: 2028 + }) + }) + }) + }) + }) + }) + + describe('Review Order', () => { + test('calls goToNextStep when Review Order clicked and billing form valid', async () => { + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [ + { + shipmentId: 'me', + shippingMethod: {c_storePickupEnabled: false}, + shippingAddress: { + address1: '123 Main St', + city: 'Tampa', + countryCode: 'US', + firstName: 'Test', + lastName: 'User', + postalCode: '33712', + stateCode: 'FL' + } + } + ], + billingAddress: { + address1: '123 Main St', + city: 'Tampa', + countryCode: 'US', + firstName: 'Test', + lastName: 'User', + postalCode: '33712', + stateCode: 'FL' + }, + paymentInstruments: [ + { + paymentInstrumentId: 'pi1', + paymentCard: { + cardType: 'Visa', + numberLastDigits: '1111', + expirationMonth: 12, + expirationYear: 2028 + } + } + ] + }) + + const {user} = renderWithProviders() + await user.click(screen.getByRole('button', {name: 'Review Order'})) + + await waitFor(() => { + expect(mockUpdateBillingAddress).toHaveBeenCalled() + expect(mockGoToNextStep).toHaveBeenCalled() + }) + }) + }) + + describe('Remove payment', () => { + test('calls removePaymentInstrumentFromBasket when Remove is clicked', async () => { + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [{shipmentId: 'me', shippingMethod: {c_storePickupEnabled: false}}], + billingAddress: {}, + paymentInstruments: [ + { + paymentInstrumentId: 'pi1', + paymentCard: { + cardType: 'Visa', + numberLastDigits: '1111', + expirationMonth: 12, + expirationYear: 2028 + } + } + ] + }) + + const {user} = renderWithProviders() + await user.click(screen.getByRole('button', {name: 'Remove'})) + + await waitFor(() => { + expect(mockRemovePaymentInstrument).toHaveBeenCalledWith({ + parameters: { + basketId: 'basket-1', + paymentInstrumentId: 'pi1' + } + }) + }) + }) + + test('calls showToast on error when remove payment fails', async () => { + mockRemovePaymentInstrument.mockRejectedValueOnce(new Error('Network error')) + + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [{shipmentId: 'me', shippingMethod: {c_storePickupEnabled: false}}], + billingAddress: {}, + paymentInstruments: [ + { + paymentInstrumentId: 'pi1', + paymentCard: { + cardType: 'Visa', + numberLastDigits: '1111', + expirationMonth: 12, + expirationYear: 2028 + } + } + ] + }) + + const {user} = renderWithProviders() + await user.click(screen.getByRole('button', {name: 'Remove'})) + + await waitFor(() => { + expect(mockShowToast).toHaveBeenCalledWith( + expect.objectContaining({ + status: 'error', + title: expect.any(String) + }) + ) + }) + }) + }) + + describe('billing same as shipping', () => { + test('uses shipping address for billing when checkbox checked', async () => { + const shippingAddress = { + address1: '123 Main St', + city: 'Tampa', + countryCode: 'US', + firstName: 'Test', + lastName: 'User', + postalCode: '33712', + stateCode: 'FL' + } + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [ + { + shipmentId: 'me', + shippingMethod: {c_storePickupEnabled: false}, + shippingAddress + } + ], + billingAddress: null, + paymentInstruments: [ + { + paymentInstrumentId: 'pi1', + paymentCard: { + cardType: 'Visa', + numberLastDigits: '1111', + expirationMonth: 12, + expirationYear: 2028 + } + } + ] + }) + + const {user} = renderWithProviders() + await user.click(screen.getByRole('button', {name: 'Review Order'})) + + await waitFor(() => { + expect(mockUpdateBillingAddress).toHaveBeenCalledWith({ + parameters: {basketId: 'basket-1'}, + body: expect.objectContaining({ + address1: '123 Main St', + city: 'Tampa', + countryCode: 'US', + postalCode: '33712', + stateCode: 'FL' + }) + }) + }) + }) + }) + + describe('PaymentCardSummary', () => { + test('displays card type, masked number and expiration', () => { + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [{shipmentId: 'me', shippingMethod: {c_storePickupEnabled: false}}], + billingAddress: {}, + paymentInstruments: [ + { + paymentInstrumentId: 'pi1', + paymentCard: { + cardType: 'Master Card', + numberLastDigits: '9999', + expirationMonth: 1, + expirationYear: 2026 + } + } + ] + }) + + renderWithProviders() + expect(screen.getByText('Master Card')).toBeInTheDocument() + expect(screen.getByText(/9999/)).toBeInTheDocument() + expect(screen.getByText('1/2026')).toBeInTheDocument() + }) + }) + + describe('empty basket', () => { + test('renders without crashing when basket is null', () => { + setUseCurrentBasketData(null) + expect(() => renderWithProviders()).not.toThrow() + }) + + test('renders without crashing when basket has no shipments', () => { + setUseCurrentBasketData({ + basketId: 'basket-1', + shipments: [], + billingAddress: null, + paymentInstruments: [] + }) + expect(() => renderWithProviders()).not.toThrow() + }) + }) +})