Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 26 additions & 25 deletions packages/template-retail-react-app/app/components/_app/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {SkipNavLink, SkipNavContent} from '@chakra-ui/skip-nav'

// Contexts
import {CurrencyProvider} from '@salesforce/retail-react-app/app/contexts'
import {BonusProductModalProvider} from '@salesforce/retail-react-app/app/hooks/use-bonus-product-modal'

// Local Project Components
import Header from '@salesforce/retail-react-app/app/components/header'
Expand Down Expand Up @@ -413,32 +414,32 @@ const App = (props) => {
</Box>
{!isOnline && <OfflineBanner />}
<AddToCartModalProvider>
<SkipNavContent
style={{
display: 'flex',
flexDirection: 'column',
flex: 1,
outline: 0
}}
>
<Box
as="main"
id="app-main"
role="main"
display="flex"
flexDirection="column"
flex="1"
<BonusProductModalProvider basket={basket}>
<SkipNavContent
style={{
display: 'flex',
flexDirection: 'column',
flex: 1,
outline: 0
}}
>
<OfflineBoundary isOnline={false}>
{children}
</OfflineBoundary>
</Box>
</SkipNavContent>

{!isCheckout ? <Footer /> : <CheckoutFooter />}

<AuthModal {...authModal} />
<DntNotification {...dntNotification} />
<Box
as="main"
id="app-main"
role="main"
display="flex"
flexDirection="column"
flex="1"
>
<OfflineBoundary isOnline={false}>
{children}
</OfflineBoundary>
</Box>
</SkipNavContent>
{!isCheckout ? <Footer /> : <CheckoutFooter />}
<AuthModal {...authModal} />
<DntNotification {...dntNotification} />
</BonusProductModalProvider>
</AddToCartModalProvider>
</Box>
</CurrencyProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '@salesforce/retail-react-app/app/components/shared/ui'
import {useCurrency, useDerivedProduct} from '@salesforce/retail-react-app/app/hooks'
import {useAddToCartModalContext} from '@salesforce/retail-react-app/app/hooks/use-add-to-cart-modal'
import {useBonusProductModalContext} from '@salesforce/retail-react-app/app/hooks/use-bonus-product-modal'

// project components
import ImageGallery from '@salesforce/retail-react-app/app/components/image-gallery'
Expand Down Expand Up @@ -131,6 +132,13 @@ const ProductView = forwardRef(
onOpen: onAddToCartModalOpen,
onClose: onAddToCartModalClose
} = useAddToCartModalContext()
const {
isOpen: isBonusProductModalOpen,
onOpen: onBonusProductModalOpen,
onClose: onBonusProductModalClose,
bonusProducts,
addBonusProducts
} = useBonusProductModalContext()
const theme = useTheme()
const [showOptionsMessage, toggleShowOptionsMessage] = useState(false)
const {
Expand Down Expand Up @@ -272,16 +280,46 @@ const ProductView = forwardRef(
return
}
try {
const itemsAdded = await addToCart(variant, quantity)
// Open modal only when `addToCart` returns some data
// It's possible that the item has been added to cart, but we don't want to open the modal.
// See wishlist_primary_action for example.
if (itemsAdded) {
onAddToCartModalOpen({
product,
itemsAdded,
selectedQuantity: quantity
})
const addToCartResponse = await addToCart(variant, quantity)

// For regular products: addToCartResponse has productSelectionValues and possibly bonusDiscountLineItems
// For product bundles: addToCartResponse is just the childProductSelections array
const itemsAdded =
addToCartResponse?.productSelectionValues || addToCartResponse
const isValidResponse =
itemsAdded && (Array.isArray(itemsAdded) || itemsAdded.length > 0)

// Compare existing bonus products with new bonus discount line items
// Only regular products (not bundles) can have bonusDiscountLineItems
const newBonusItems =
addToCartResponse?.bonusDiscountLineItems?.filter(
(newItem) =>
Copy link
Contributor

@sf-jhalak-maheshwari sf-jhalak-maheshwari Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change name to bonusItem?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can move this to within the "isValidResponse" check, right?

!bonusProducts.some(
(existingItem) => existingItem.id === newItem.id
)
) || []

if (isValidResponse) {
// Show bonus product modal first if there are bonus items
if (newBonusItems?.length > 0) {
// Update bonusProducts list with the new bonus items
addBonusProducts(newBonusItems)
onBonusProductModalOpen({
newBonusItems,
allBonusItems: addToCartResponse.bonusDiscountLineItems,
openAddToCartModalIfNeeded: true,
product,
itemsAdded,
selectedQuantity: quantity
})
} else {
// If no bonus items, just show add to cart modal
onAddToCartModalOpen({
product,
itemsAdded,
selectedQuantity: quantity
})
}
}
} catch (e) {
showError()
Expand Down Expand Up @@ -366,6 +404,12 @@ const ProductView = forwardRef(
}
}, [location.pathname])

useEffect(() => {
if (isBonusProductModalOpen) {
onBonusProductModalClose()
}
}, [location.pathname])

useEffect(() => {
if (
!isProductASet &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,13 @@ afterEach(() => {
sessionStorage.clear()
})

const mockBasket = {bonusDiscountLineItems: []}

test('ProductView Component renders properly', async () => {
const addToCart = jest.fn()
renderWithProviders(<MockComponent product={mockProductDetail} addToCart={addToCart} />)
renderWithProviders(<MockComponent product={mockProductDetail} addToCart={addToCart} />, {
wrapperProps: {basket: mockBasket}
})
expect(screen.getAllByText(/Black Single Pleat Athletic Fit Wool Suit/i)).toHaveLength(2)
expect(screen.getAllByText(/299\.99/)).toHaveLength(4)
expect(screen.getAllByText(/Add to cart/i)).toHaveLength(2)
Expand All @@ -58,7 +62,9 @@ test('ProductView Component renders properly', async () => {

test('ProductView Component renders with addToCart event handler', async () => {
const addToCart = jest.fn()
await renderWithProviders(<MockComponent product={mockProductDetail} addToCart={addToCart} />)
await renderWithProviders(<MockComponent product={mockProductDetail} addToCart={addToCart} />, {
wrapperProps: {basket: mockBasket}
})

const addToCartButton = screen.getAllByText(/add to cart/i)[0]
fireEvent.click(addToCartButton)
Expand All @@ -72,7 +78,10 @@ test('ProductView Component renders with addToWishList event handler', async ()
const addToWishlist = jest.fn()

await renderWithProviders(
<MockComponent product={mockProductDetail} addToWishlist={addToWishlist} />
<MockComponent product={mockProductDetail} addToWishlist={addToWishlist} />,
{
wrapperProps: {basket: mockBasket}
}
)

await waitFor(() => {
Expand All @@ -91,7 +100,10 @@ test('ProductView Component renders with updateWishlist event handler', async ()
const updateWishlist = jest.fn()

await renderWithProviders(
<MockComponent product={mockProductDetail} updateWishlist={updateWishlist} />
<MockComponent product={mockProductDetail} updateWishlist={updateWishlist} />,
{
wrapperProps: {basket: mockBasket}
}
)

await waitFor(() => {
Expand All @@ -109,7 +121,9 @@ test('ProductView Component renders with updateWishlist event handler', async ()
test('Product View can update quantity', async () => {
const user = userEvent.setup()
const addToCart = jest.fn()
await renderWithProviders(<MockComponent product={mockProductDetail} addToCart={addToCart} />)
await renderWithProviders(<MockComponent product={mockProductDetail} addToCart={addToCart} />, {
wrapperProps: {basket: mockBasket}
})

let quantityBox
await waitFor(() => {
Expand All @@ -132,7 +146,9 @@ test('Product View handles invalid quantity inputs', async () => {
const user = userEvent.setup()

// Any invalid input should be reset to minOrderQuantity
await renderWithProviders(<MockComponent product={mockProductDetail} />)
await renderWithProviders(<MockComponent product={mockProductDetail} />, {
wrapperProps: {basket: mockBasket}
})

const quantityInput = screen.getByRole('spinbutton', {name: /quantity/i})
const minQuantity = mockProductDetail.minOrderQuantity.toString()
Expand All @@ -156,7 +172,9 @@ test('Product View handles invalid quantity inputs', async () => {
describe('ProductView Component', () => {
test('increases quantity when increment button is clicked', async () => {
const user = userEvent.setup()
renderWithProviders(<ProductView product={mockProductDetail} />)
renderWithProviders(<ProductView product={mockProductDetail} />, {
wrapperProps: {basket: mockBasket}
})

const quantityInput = await screen.findByRole('spinbutton')
const incrementButton = screen.getByTestId('quantity-increment')
Expand All @@ -179,7 +197,10 @@ describe('ProductView Component', () => {
test('renders a product set properly - parent item', () => {
const parent = mockProductSet
renderWithProviders(
<MockComponent product={parent} addToCart={() => {}} addToWishlist={() => {}} />
<MockComponent product={parent} addToCart={() => {}} addToWishlist={() => {}} />,
{
wrapperProps: {basket: mockBasket}
}
)

// NOTE: there can be duplicates of the same element, due to mobile and desktop views
Expand All @@ -204,7 +225,10 @@ test('renders a product set properly - parent item', () => {
test('renders a product set properly - child item', () => {
const child = mockProductSet.setProducts[0]
renderWithProviders(
<MockComponent product={child} addToCart={() => {}} addToWishlist={() => {}} />
<MockComponent product={child} addToCart={() => {}} addToWishlist={() => {}} />,
{
wrapperProps: {basket: mockBasket}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing this getting reused in every test case, if possible, can we move this to beforeEach?

)

// NOTE: there can be duplicates of the same element, due to mobile and desktop views
Expand Down Expand Up @@ -239,7 +263,10 @@ test('validateOrderability callback is called when adding a set to cart', async
validateOrderability={validateOrderability}
addToCart={() => {}}
addToWishlist={() => {}}
/>
/>,
{
wrapperProps: {basket: mockBasket}
}
)

const button = screen.getByRole('button', {name: /add set to cart/i})
Expand All @@ -262,7 +289,10 @@ test('onVariantSelected callback is called after successfully selected a variant
onVariantSelected={onVariantSelected}
addToCart={() => {}}
addToWishlist={() => {}}
/>
/>,
{
wrapperProps: {basket: mockBasket}
}
)

const size = screen.getByRole('radio', {name: /xl/i})
Expand All @@ -280,7 +310,10 @@ describe('add to cart button loading tests', () => {
product={mockProductDetail}
addToCart={() => {}}
isBasketLoading={true}
/>
/>,
{
wrapperProps: {basket: mockBasket}
}
)
expect(screen.getByRole('button', {name: /add to cart/i})).toBeDisabled()
})
Expand All @@ -291,7 +324,10 @@ describe('add to cart button loading tests', () => {
product={mockProductDetail}
addToCart={() => {}}
isBasketLoading={false}
/>
/>,
{
wrapperProps: {basket: mockBasket}
}
)
expect(screen.getByRole('button', {name: /add to cart/i})).toBeEnabled()
})
Expand All @@ -300,7 +336,10 @@ describe('add to cart button loading tests', () => {
test('renders a product bundle properly - parent item', () => {
const parent = mockProductBundle
renderWithProviders(
<MockComponent product={parent} addToCart={() => {}} addToWishlist={() => {}} />
<MockComponent product={parent} addToCart={() => {}} addToWishlist={() => {}} />,
{
wrapperProps: {basket: mockBasket}
}
)

// NOTE: there can be duplicates of the same element, due to mobile and desktop views
Expand Down Expand Up @@ -330,7 +369,10 @@ test('renders a product bundle properly - child item', () => {
addToWishlist={() => {}}
isProductPartOfBundle={true}
setChildProductOrderability={() => {}}
/>
/>,
{
wrapperProps: {basket: mockBasket}
}
)

const addToCartButton = screen.queryByRole('button', {name: /add to cart/i})
Expand Down
Loading