Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions packages/template-retail-react-app/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Enhanced the shopping assistant that integrates Salesforce Embedded Messaging Service with PWA Kit applications, adding comprehensive context support, localization capabilities, and improved user experience features. [#3259](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3259)
- [Breaking] Removed domainUrl, locale, basetId properties as part off the ShopperAgent during initialization. [#3259](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3259)
- Added support for Choice of Bonus Products feature. Users can now select from available bonus products when they qualify for the associated promotion. The bonus product selection flow can be entered from either the "Item Added to Cart" modal (when adding the qualifying product to the cart) or from the cart page. [#3292] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3292)
- Fixed shipping method section expanding unnecessarily when default options are auto-selected.[#3345] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3345)

## v8.0.0 (Sep 04, 2025)
- Add support for environment level base paths on /mobify routes [#2892](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2892)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,8 @@ test('Can edit address during checkout as a registered customer', async () => {
expect(screen.getByTestId('sf-toggle-card-step-2-content')).not.toBeEmptyDOMElement()
})

expect(screen.getByText('369 Main Street')).toBeInTheDocument()
const shippingAddressCard = screen.getByTestId('sf-toggle-card-step-1-content')
expect(within(shippingAddressCard).getByText('369 Main Street')).toBeInTheDocument()
})

test('Can add address during checkout as a registered customer', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,34 @@ export default function ShippingMethods() {
)
if (hasNewFields) {
form.reset(newDefaults)
deliveryShipments.forEach(async (shipment) => {
const methodId = newDefaults[`shippingMethodId_${shipment.shipmentId}`]
const hasMethodInBasket = shipment.shippingMethod && shipment.shippingMethod.id

// auto-submit if;
// - default method to submit present
// - the shipment doesn't already have a method in basket
// - user hasn't manually selected
if (
methodId &&
!hasMethodInBasket &&
methodId === shippingMethods?.defaultShippingMethodId
) {
try {
await updateShippingMethod.mutateAsync({
parameters: {
basketId: basket.basketId,
shipmentId: shipment.shipmentId
},
body: {
id: methodId
}
})
} catch (error) {
console.warn(error)
}
}
})
}
}, [deliveryShipments.length, shippingMethods?.defaultShippingMethodId])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import ShippingMethods from '@salesforce/retail-react-app/app/pages/checkout/par
import {useCheckout} from '@salesforce/retail-react-app/app/pages/checkout/util/checkout-context'
import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
import {useCurrency} from '@salesforce/retail-react-app/app/hooks'
import {useShippingMethodsForShipment, useProducts} from '@salesforce/commerce-sdk-react'
import {
useShippingMethodsForShipment,
useProducts,
useShopperBasketsMutation
} from '@salesforce/commerce-sdk-react'

// Mock the hooks
jest.mock('@salesforce/retail-react-app/app/pages/checkout/util/checkout-context')
Expand All @@ -25,6 +29,7 @@ const mockUseCurrentBasket = useCurrentBasket
const mockUseCurrency = useCurrency
const mockUseShippingMethodsForShipment = useShippingMethodsForShipment
const mockUseProducts = useProducts
const mockUseShopperBasketsMutation = useShopperBasketsMutation

// Mock data
const mockBasket = {
Expand Down Expand Up @@ -169,6 +174,7 @@ describe('ShippingMethods', () => {
data: mockProductsMap,
isLoading: false
})
mockUseShopperBasketsMutation.mockReturnValue(jest.fn().mockResolvedValue({}))
})

afterEach(() => {
Expand Down Expand Up @@ -538,4 +544,160 @@ describe('ShippingMethods', () => {
expect(screen.getByText('Standard Shipping')).toBeInTheDocument()
})
})

describe('auto-submit functionality', () => {
test('should auto-submit default shipping method when available', async () => {
const basketWithoutMethods = {
...mockBasket,
shipments: [
{
...mockBasket.shipments[0],
shippingMethod: null
}
]
}

const mockShippingMethods = {
defaultShippingMethodId: 'default-shipping-method',
applicableShippingMethods: [
{
id: 'default-shipping-method',
name: 'Default Shipping'
}
]
}

const mockMutateAsync = jest.fn().mockResolvedValue({})
mockUseShopperBasketsMutation.mockReturnValue({
updateShippingMethod: {mutateAsync: mockMutateAsync}
})

// after auto-submit, step should advance to PAYMENT (summary mode)
mockUseCheckout.mockReturnValue({
step: 'PAYMENT',
STEPS: {SHIPPING_OPTIONS: 'SHIPPING_OPTIONS', PAYMENT: 'PAYMENT'},
goToStep: jest.fn(),
goToNextStep: jest.fn()
})

mockUseCurrentBasket.mockReturnValue({
data: basketWithoutMethods,
derivedData: {
totalShippingCost: 5.99,
isMissingShippingMethod: false
},
isLoading: false
})

mockUseShippingMethodsForShipment.mockReturnValue({
data: mockShippingMethods,
isLoading: false
})

renderWithIntl(<ShippingMethods />)

// component is in SUMMARY mode (collapsed) after auto-submit
expect(screen.getByRole('button', {name: 'Edit Shipping Options'})).toBeInTheDocument()
expect(
screen.queryByRole('radio', {name: 'Default Shipping $5.99'})
).not.toBeInTheDocument()
expect(
screen.queryByRole('button', {name: 'Continue to Payment'})
).not.toBeInTheDocument()
})

test('should not auto-submit if shipment already has a method', async () => {
// Mock basket that already has a shipping method
const basketWithMethod = {
...mockBasket,
shipments: [
{
...mockBasket.shipments[0],
shippingMethod: {
id: 'existing-method',
name: 'Existing Shipping'
}
}
]
}

const mockUpdateShippingMethod = jest.fn().mockResolvedValue({})
mockUpdateShippingMethod.mutateAsync = jest.fn().mockResolvedValue({})

// Mock the mutation hook
mockUseShopperBasketsMutation.mockReturnValue(mockUpdateShippingMethod)

mockUseCurrentBasket.mockReturnValue({
data: basketWithMethod,
derivedData: {totalShippingCost: 0},
isLoading: false
})

mockUseShippingMethodsForShipment.mockReturnValue({
data: {
defaultShippingMethodId: 'default-method',
applicableShippingMethods: []
},
isLoading: false
})

renderWithIntl(<ShippingMethods />)

// no auto-submit happens
await waitFor(() => {
expect(mockUpdateShippingMethod).not.toHaveBeenCalled()
})
})

test('should not auto-submit if user has manually selected a different method', async () => {
const basketWithoutMethods = {
...mockBasket,
shipments: [
{
...mockBasket.shipments[0],
shippingMethod: null
}
]
}

// Mock shipping methods with default
const mockShippingMethods = {
defaultShippingMethodId: 'default-method',
applicableShippingMethods: [
{
id: 'default-method',
name: 'Default Shipping'
},
{
id: 'user-selected-method',
name: 'User Selected Shipping'
}
]
}

const mockUpdateShippingMethod = jest.fn().mockResolvedValue({})
mockUpdateShippingMethod.mutateAsync = jest.fn().mockResolvedValue({})

// Mock the mutation hook
mockUseShopperBasketsMutation.mockReturnValue(mockUpdateShippingMethod)

mockUseCurrentBasket.mockReturnValue({
data: basketWithoutMethods,
derivedData: {totalShippingCost: 0},
isLoading: false
})

mockUseShippingMethodsForShipment.mockReturnValue({
data: mockShippingMethods,
isLoading: false
})

renderWithIntl(<ShippingMethods />)

// no auto-submit happens because the form would have user-selected-method, not default-method)
await waitFor(() => {
expect(mockUpdateShippingMethod).not.toHaveBeenCalled()
})
})
})
})
Loading