Skip to content

Commit d1f9aa1

Browse files
authored
W-19151166 fix product view modal cart (#2924)
* fix product view modal on cart page
1 parent a3da40f commit d1f9aa1

File tree

10 files changed

+90
-57
lines changed

10 files changed

+90
-57
lines changed

packages/template-chakra-storefront/CHANGELOG.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
## 0.1.0-extensibility-preview.5
2-
- Fix failing tests in pages folder [#2872](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2872)
3-
- Fix component tests in storefront template [#2878](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2878)
42
- Migrate directory structure from `app/*` to `src/*` [#2693](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2693)
5-
- Upgrade to Chakra UI v3 [2839](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2839)
6-
- Create a safe version of `<Portal>` that won't break the SSR rendering [#2785](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2785)
3+
- Upgrade to Chakra UI v3 and Decomposition on Cart, PLP, PDP and Cart [2839](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2839), [#2872](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2872), [#2878](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2878), [#2924](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2924)
4+
- Create a safe version of `<Portal>` that won't break the SSR rendering [#2785](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2785)
75

86
## 0.1.0-extensibility-preview.4
97
- Fix hreflang alternate links [#2269](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2269)

packages/template-chakra-storefront/src/components/loading-spinner/index.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const LoadingSpinner = ({wrapperStyles = {}, spinnerStyles = {}}) => {
1717
left="0"
1818
right="0"
1919
bottom="0"
20+
data-testid="loading-spinner"
2021
background="whiteAlpha.800"
2122
{...wrapperStyles}
2223
>

packages/template-chakra-storefront/src/components/product-view-modal/bundle.jsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
/*
2-
* Copyright (c) 2023, Salesforce, Inc.
2+
* Copyright (c) 2025, Salesforce, Inc.
33
* All rights reserved.
44
* SPDX-License-Identifier: BSD-3-Clause
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

88
import React, {useState, useRef} from 'react'
99
import PropTypes from 'prop-types'
10-
import {Dialog, Flex, Box, VStack, useBreakpointValue} from '@chakra-ui/react'
10+
import {useIntl} from 'react-intl'
11+
import {Box, CloseButton, Dialog, Flex, VStack, useBreakpointValue} from '@chakra-ui/react'
1112
import {keepPreviousData} from '@tanstack/react-query'
13+
import {useProducts} from '@salesforce/commerce-sdk-react'
14+
15+
// Project Components
1216
import ProductView from '../../components/product-view'
13-
import {useProductViewModal} from '../../hooks/use-product-view-modal'
1417
import SafePortal from '../safe-portal'
15-
import {useProducts} from '@salesforce/commerce-sdk-react'
1618
import ImageGallery, {Skeleton as ImageGallerySkeleton} from '../../components/image-gallery'
19+
20+
// Project hooks
21+
import {useProductViewModal} from '../../hooks/use-product-view-modal'
1722
import {useDerivedProduct} from '../../hooks'
18-
import {useIntl} from 'react-intl'
1923

2024
/**
2125
* A Dialog that contains Product View for product bundle
@@ -58,16 +62,16 @@ const BundleProductViewModal = ({product: bundle, isOpen, onClose, updateCart, .
5862

5963
return (
6064
<Dialog.Root
65+
lazyMount
6166
open={isOpen}
6267
onOpenChange={() => onClose()}
63-
size="4xl"
68+
size="xl"
6469
closeOnInteractOutside={false}
6570
>
6671
<SafePortal>
6772
<Dialog.Backdrop />
6873
<Dialog.Positioner>
6974
<Dialog.Content data-testid="product-view-modal" aria-label={label}>
70-
<Dialog.CloseTrigger />
7175
<Dialog.Body pb={8} bg="white" paddingBottom={6} marginTop={6}>
7276
<Flex direction={['column', 'column', 'column', 'row']}>
7377
{/* Due to desktop layout, we'll need to render the image gallery separately, from outside the ProductView */}
@@ -169,6 +173,9 @@ const BundleProductViewModal = ({product: bundle, isOpen, onClose, updateCart, .
169173
</VStack>
170174
</Flex>
171175
</Dialog.Body>
176+
<Dialog.CloseTrigger asChild>
177+
<CloseButton size="sm" />
178+
</Dialog.CloseTrigger>
172179
</Dialog.Content>
173180
</Dialog.Positioner>
174181
</SafePortal>

packages/template-chakra-storefront/src/components/product-view-modal/bundle.test.js

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
mockProductBundleWithVariants,
1717
mockProductBundle
1818
} from '../../../mocks/product-bundle'
19-
import {rest} from 'msw'
19+
import {prependHandlersToServer} from '../../../jest-setup'
2020

2121
const MockComponent = ({updateCart}) => {
2222
const {open, onOpen, onClose} = useDisclosure()
@@ -40,40 +40,46 @@ MockComponent.propTypes = {
4040
}
4141

4242
beforeEach(() => {
43-
global.server.use(
44-
rest.get('*/products/:productId', (req, res, ctx) => {
45-
return res(ctx.delay(0), ctx.status(200), ctx.json(mockProductBundle))
46-
}),
47-
rest.get('*/products', (req, res, ctx) => {
48-
const swingTankBlackMediumVariantId = '701643473915M'
49-
const swingTankBlackLargeVariantId = '701643473908M'
50-
if (req.url.toString().includes(swingTankBlackMediumVariantId)) {
51-
mockProductBundleWithVariants.data[1].inventory = {
52-
...mockProductBundleWithVariants.data[1].inventory,
53-
stockLevel: 0
54-
}
55-
} else if (req.url.toString().includes(swingTankBlackLargeVariantId)) {
56-
mockProductBundleWithVariants.data[1].inventory = {
57-
...mockProductBundleWithVariants.data[1].inventory,
58-
stockLevel: 1
43+
prependHandlersToServer([
44+
{
45+
path: '*/products/:productId',
46+
method: 'get',
47+
res: () => mockProductBundle
48+
},
49+
{
50+
path: '*/products',
51+
method: 'get',
52+
res: (req) => {
53+
const swingTankBlackMediumVariantId = '701643473915M'
54+
const swingTankBlackLargeVariantId = '701643473908M'
55+
if (req.url.toString().includes(swingTankBlackMediumVariantId)) {
56+
mockProductBundleWithVariants.data[1].inventory = {
57+
...mockProductBundleWithVariants.data[1].inventory,
58+
stockLevel: 0
59+
}
60+
} else if (req.url.toString().includes(swingTankBlackLargeVariantId)) {
61+
mockProductBundleWithVariants.data[1].inventory = {
62+
...mockProductBundleWithVariants.data[1].inventory,
63+
stockLevel: 1
64+
}
5965
}
66+
return mockProductBundleWithVariants
6067
}
61-
return res(ctx.json(mockProductBundleWithVariants))
62-
})
63-
)
68+
}
69+
])
6470
})
6571

6672
afterEach(() => {
6773
jest.resetModules()
6874
jest.restoreAllMocks()
6975
})
70-
//TODO: fix failed tests
71-
test.skip('renders bundle product view modal', async () => {
72-
renderWithProviders(<MockComponent />)
76+
77+
test('renders bundle product view modal', async () => {
78+
const {user} = renderWithProviders(<MockComponent />)
7379
await waitFor(async () => {
7480
const trigger = screen.getByText(/open modal/i)
7581
await act(async () => {
76-
fireEvent.click(trigger)
82+
user.click(trigger)
7783
})
7884
})
7985

@@ -92,15 +98,14 @@ test.skip('renders bundle product view modal', async () => {
9298
})
9399
})
94100

95-
test.skip('renders bundle product view modal with handleUpdateCart handler', async () => {
101+
test('renders bundle product view modal with handleUpdateCart handler', async () => {
96102
const handleUpdateCart = jest.fn()
97-
renderWithProviders(<MockComponent updateCart={handleUpdateCart} />)
103+
const {user} = renderWithProviders(<MockComponent updateCart={handleUpdateCart} />)
98104

99-
// open the modal
100105
await waitFor(async () => {
101106
const trigger = screen.getByText(/open modal/i)
102107
await act(async () => {
103-
fireEvent.click(trigger)
108+
user.click(trigger)
104109
})
105110
})
106111

@@ -115,12 +120,12 @@ test.skip('renders bundle product view modal with handleUpdateCart handler', asy
115120
expect(handleUpdateCart).toHaveBeenCalledTimes(1)
116121
})
117122

118-
test.skip('bundle product view modal disables update button when child is out of stock', async () => {
119-
renderWithProviders(<MockComponent />)
123+
test('bundle product view modal disables update button when child is out of stock', async () => {
124+
const {user} = renderWithProviders(<MockComponent />)
120125
await waitFor(async () => {
121126
const trigger = screen.getByText(/open modal/i)
122127
await act(async () => {
123-
fireEvent.click(trigger)
128+
user.click(trigger)
124129
})
125130
})
126131

@@ -151,13 +156,13 @@ test.skip('bundle product view modal disables update button when child is out of
151156
expect(screen.getByText('Out of stock')).toBeInTheDocument()
152157
})
153158
})
154-
159+
//TODO: fix this failing test
155160
test.skip('bundle product view modal disables update button when quantity exceeds child inventory', async () => {
156-
renderWithProviders(<MockComponent />)
161+
const {user} = renderWithProviders(<MockComponent />)
157162
await waitFor(async () => {
158163
const trigger = screen.getByText(/open modal/i)
159164
await act(async () => {
160-
fireEvent.click(trigger)
165+
user.click(trigger)
161166
})
162167
})
163168

packages/template-chakra-storefront/src/components/product-view-modal/index.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ const ProductViewModal = ({product, isOpen, onClose, ...props}) => {
3939
<Dialog.Backdrop />
4040
<Dialog.Positioner>
4141
<Dialog.Content data-testid="product-view-modal" aria-label={label}>
42-
<Dialog.CloseTrigger />
4342
<Dialog.Body pb={8} bg="white" paddingBottom={6} marginTop={6}>
4443
<ProductView
4544
showFullLink={true}

packages/template-chakra-storefront/src/hooks/use-product-view-modal.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import useToast from './use-toast'
1313
import {useIntl} from 'react-intl'
1414
import {API_ERROR_MESSAGE} from '../../config/constants'
1515
import {useProduct} from '@salesforce/commerce-sdk-react'
16+
import {keepPreviousData} from '@tanstack/react-query'
1617

1718
/**
1819
* This hook is responsible for fetching a product detail based on the variation selection
@@ -35,7 +36,7 @@ export const useProductViewModal = (initialProduct) => {
3536
} = useProduct(
3637
{parameters: {id: (variant || product)?.productId}},
3738
{
38-
placeholderData: initialProduct,
39+
placeholderData: keepPreviousData,
3940
select: (data) => {
4041
// if the product id is the same as the initial product id,
4142
// then merge the data with the initial product to be able to show correct quantity in the modal

packages/template-chakra-storefront/src/pages/cart/hooks/use-cart-operations.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {useShopperBasketsMutation} from '@salesforce/commerce-sdk-react'
1111
import useToast from '../../../hooks/use-toast'
1212
import {TOAST_MESSAGE_REMOVED_ITEM_FROM_CART} from '../../../../config/constants'
1313
import {getUpdateBundleChildArray} from '../../../utils/product-utils'
14+
import {useDisclosure} from '@chakra-ui/react'
1415

1516
const DEBOUNCE_WAIT = 750
1617

@@ -26,6 +27,9 @@ export const useCartOperations = (basket, productsByItemId, showError) => {
2627
const [localQuantity, setLocalQuantity] = useState({})
2728
const [isCartItemLoading, setCartItemLoading] = useState(false)
2829

30+
// Modal state and actions
31+
const {open: isOpen, onOpen, onClose} = useDisclosure()
32+
2933
const {formatMessage} = useIntl()
3034
const toast = useToast()
3135

@@ -39,6 +43,8 @@ export const useCartOperations = (basket, productsByItemId, showError) => {
3943
// using try-catch is better than using onError callback since we have many mutation calls logic here
4044
try {
4145
setCartItemLoading(true)
46+
// close the modal before performing any actions on cart item
47+
onClose()
4248
const productIds = basket.productItems.map(({productId}) => productId)
4349

4450
// The user is selecting different variant, and it has not existed in basket
@@ -88,6 +94,8 @@ export const useCartOperations = (basket, productsByItemId, showError) => {
8894
const handleUpdateBundle = async (bundle, bundleQuantity, childProducts) => {
8995
try {
9096
setCartItemLoading(true)
97+
// close the modal before performing any actions on cart item
98+
onClose()
9199
const itemsToBeUpdated = getUpdateBundleChildArray(bundle, childProducts)
92100

93101
// We only update the parent bundle when the quantity changes
@@ -221,6 +229,9 @@ export const useCartOperations = (basket, productsByItemId, showError) => {
221229
}
222230

223231
return {
232+
isOpen,
233+
onClose,
234+
onOpen,
224235
selectedItem,
225236
setSelectedItem,
226237
localQuantity,

packages/template-chakra-storefront/src/pages/cart/index.jsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ const Cart = () => {
3838

3939
// Cart operations
4040
const {
41+
isOpen: isProductViewModalOpen,
42+
onClose: onProductViewModalClose,
43+
onOpen: onProductViewModalOpen,
4144
selectedItem,
4245
setSelectedItem,
4346
localQuantity,
@@ -65,18 +68,15 @@ const Cart = () => {
6568
// Shipping
6669
useCartDefaultShipping(basket)
6770

68-
// Modal state
69-
const {open: isOpen, onOpen, onClose} = useDisclosure()
70-
7171
// Handle edit click
7272
const handleEditClick = (product) => {
7373
setSelectedItem(product)
74-
onOpen()
74+
onProductViewModalOpen()
7575
}
7676

7777
// Handle modal close
7878
const handleModalClose = () => {
79-
onClose()
79+
onProductViewModalClose()
8080
setSelectedItem(undefined)
8181
}
8282

@@ -137,8 +137,8 @@ const Cart = () => {
137137

138138
{/* Modals */}
139139
<CartModals
140-
isOpen={isOpen}
141-
onOpen={onOpen}
140+
isOpen={isProductViewModalOpen}
141+
onOpen={onProductViewModalOpen}
142142
onClose={handleModalClose}
143143
selectedItem={selectedItem}
144144
handleUpdateCart={handleUpdateCart}

packages/template-chakra-storefront/src/pages/cart/index.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ describe('Product view tests', function () {
259259
{
260260
path: '*/baskets/*/items/*',
261261
method: 'patch',
262+
// give a bit of delay to get the loading spinner shows up
263+
delay: 500,
262264
res: () => {
263265
// Return updated basket with incremented quantity
264266
return {
@@ -306,6 +308,16 @@ describe('Product view tests', function () {
306308
`sf-cart-item-${mockCustomerBaskets.baskets[0].productItems[0].productId}`
307309
)
308310

311+
// Check for loading spinner appearing
312+
await waitFor(() => {
313+
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument()
314+
})
315+
316+
// Check for loading spinner disappearing
317+
await waitFor(() => {
318+
expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument()
319+
})
320+
309321
await waitFor(() => {
310322
expect(within(updatedCartItem).getByDisplayValue('3')).toBeInTheDocument()
311323
})

packages/template-chakra-storefront/src/pages/cart/partials/cart-modals.jsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ const CartModals = ({
3838
basket,
3939
handleUnavailableProducts
4040
}) => {
41-
const modalProps = useDisclosure()
42-
41+
const confirmationModalProps = useDisclosure()
4342
return (
4443
<Box>
4544
{/* Product View Modals */}
@@ -71,7 +70,7 @@ const CartModals = ({
7170
handleRemoveItem(selectedItem)
7271
}}
7372
onAlternateAction={() => {}}
74-
{...modalProps}
73+
{...confirmationModalProps}
7574
/>
7675

7776
{/* Unavailable Product Confirmation Modal */}

0 commit comments

Comments
 (0)