@salesforce/retail-react-app 7.1.0-preview.1 → 8.0.0-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +8 -2
  2. package/app/components/_app/index.jsx +9 -7
  3. package/app/components/_app/index.test.js +2 -2
  4. package/app/components/_app-config/index.jsx +9 -3
  5. package/app/components/drawer-menu/drawer-menu.jsx +3 -1
  6. package/app/components/footer/index.jsx +3 -1
  7. package/app/components/header/index.jsx +3 -1
  8. package/app/components/header/index.test.js +2 -2
  9. package/app/components/island/README.md +1 -1
  10. package/app/components/island/index.jsx +3 -1
  11. package/app/components/island/index.test.js +94 -5
  12. package/app/components/item-variant/item-attributes.jsx +12 -3
  13. package/app/components/multiship/multiship-order-summary.jsx +137 -0
  14. package/app/components/multiship/multiship-order-summary.test.js +121 -0
  15. package/app/components/order-summary/index.jsx +2 -4
  16. package/app/components/pickup-or-delivery/index.jsx +80 -0
  17. package/app/components/pickup-or-delivery/index.test.jsx +182 -0
  18. package/app/components/product-item/index.jsx +26 -16
  19. package/app/components/product-item/index.test.js +29 -2
  20. package/app/components/product-item-list/index.jsx +10 -0
  21. package/app/components/product-item-list/index.test.jsx +14 -0
  22. package/app/components/product-view/index.jsx +9 -6
  23. package/app/components/product-view/index.test.js +25 -21
  24. package/app/components/quantity-picker/index.test.jsx +12 -12
  25. package/app/components/reset-password/index.test.js +1 -1
  26. package/app/components/shared/ui/AlertDescription/index.jsx +8 -0
  27. package/app/components/shared/ui/index.jsx +1 -0
  28. package/app/components/store-display/index.jsx +28 -4
  29. package/app/components/store-display/index.test.js +71 -0
  30. package/app/components/store-locator/form.test.jsx +16 -4
  31. package/app/components/store-locator/list.jsx +9 -4
  32. package/app/components/toggle-card/index.jsx +14 -0
  33. package/app/components/unavailable-product-confirmation-modal/index.jsx +19 -5
  34. package/app/components/unavailable-product-confirmation-modal/index.test.js +122 -1
  35. package/app/constants.js +20 -6
  36. package/app/contexts/store-locator-provider.jsx +7 -1
  37. package/app/contexts/store-locator-provider.test.jsx +36 -1
  38. package/app/hooks/use-address-form.js +155 -0
  39. package/app/hooks/use-address-form.test.js +501 -0
  40. package/app/hooks/use-auth-modal.js +2 -6
  41. package/app/hooks/use-current-basket.js +71 -2
  42. package/app/hooks/use-current-basket.test.js +37 -1
  43. package/app/hooks/use-dnt-notification.js +4 -4
  44. package/app/hooks/use-dnt-notification.test.js +5 -5
  45. package/app/hooks/use-item-shipment-management.js +233 -0
  46. package/app/hooks/use-item-shipment-management.test.js +696 -0
  47. package/app/hooks/use-multiship.js +589 -0
  48. package/app/hooks/use-multiship.test.js +776 -0
  49. package/app/hooks/use-pickup-shipment.js +70 -106
  50. package/app/hooks/use-pickup-shipment.test.js +345 -209
  51. package/app/hooks/use-product-address-assignment.js +280 -0
  52. package/app/hooks/use-product-address-assignment.test.js +414 -0
  53. package/app/hooks/use-product-inventory.js +100 -0
  54. package/app/hooks/use-product-inventory.test.js +254 -0
  55. package/app/hooks/use-shipment-operations.js +168 -0
  56. package/app/hooks/use-shipment-operations.test.js +385 -0
  57. package/app/hooks/use-store-locator.js +24 -2
  58. package/app/hooks/use-store-locator.test.jsx +109 -1
  59. package/app/pages/account/index.test.js +1 -1
  60. package/app/pages/account/profile.test.js +0 -2
  61. package/app/pages/cart/index.jsx +397 -157
  62. package/app/pages/cart/index.test.js +353 -2
  63. package/app/pages/cart/partials/bonus-products-title.jsx +10 -8
  64. package/app/pages/cart/partials/cart-secondary-button-group.test.js +1 -1
  65. package/app/pages/cart/partials/order-type-display.jsx +68 -0
  66. package/app/pages/cart/partials/order-type-display.test.js +241 -0
  67. package/app/pages/checkout/confirmation.jsx +79 -158
  68. package/app/pages/checkout/index.jsx +34 -9
  69. package/app/pages/checkout/index.test.js +245 -118
  70. package/app/pages/checkout/partials/contact-info.jsx +2 -6
  71. package/app/pages/checkout/partials/contact-info.test.js +93 -7
  72. package/app/pages/checkout/partials/payment.jsx +19 -5
  73. package/app/pages/checkout/partials/pickup-address.jsx +340 -70
  74. package/app/pages/checkout/partials/pickup-address.test.js +1075 -82
  75. package/app/pages/checkout/partials/product-shipping-address-card.jsx +382 -0
  76. package/app/pages/checkout/partials/shipment-details.jsx +209 -0
  77. package/app/pages/checkout/partials/shipment-details.test.js +246 -0
  78. package/app/pages/checkout/partials/shipping-address.jsx +156 -68
  79. package/app/pages/checkout/partials/shipping-address.test.js +673 -0
  80. package/app/pages/checkout/partials/shipping-method-options.jsx +180 -0
  81. package/app/pages/checkout/partials/shipping-methods.jsx +403 -0
  82. package/app/pages/checkout/partials/shipping-methods.test.js +472 -0
  83. package/app/pages/checkout/partials/shipping-multi-address.jsx +259 -0
  84. package/app/pages/checkout/partials/shipping-multi-address.test.js +2088 -0
  85. package/app/pages/checkout/partials/shipping-product-cards.jsx +101 -0
  86. package/app/pages/checkout/util/checkout-context.js +25 -18
  87. package/app/pages/login/index.jsx +2 -6
  88. package/app/pages/product-detail/index.jsx +96 -81
  89. package/app/pages/product-detail/index.test.js +103 -19
  90. package/app/pages/product-list/index.jsx +3 -1
  91. package/app/pages/product-list/partials/inventory-filter.jsx +18 -21
  92. package/app/pages/product-list/partials/inventory-filter.test.js +15 -17
  93. package/app/pages/product-list/partials/selected-refinements.jsx +3 -1
  94. package/app/ssr.js +1 -1
  95. package/app/static/translations/compiled/en-GB.json +316 -30
  96. package/app/static/translations/compiled/en-US.json +316 -30
  97. package/app/static/translations/compiled/en-XA.json +673 -75
  98. package/app/utils/address-utils.js +112 -0
  99. package/app/utils/address-utils.test.js +484 -0
  100. package/app/utils/product-utils.js +17 -5
  101. package/app/utils/product-utils.test.js +17 -8
  102. package/app/utils/sfdc-user-agent-utils.js +32 -0
  103. package/app/utils/sfdc-user-agent-utils.test.js +82 -0
  104. package/app/utils/shipment-utils.js +196 -0
  105. package/app/utils/shipment-utils.test.js +458 -0
  106. package/app/utils/test-utils.js +4 -4
  107. package/app/utils/utils.js +6 -1
  108. package/config/default.js +4 -1
  109. package/config/mocks/default.js +3 -1
  110. package/package.json +9 -9
  111. package/translations/en-GB.json +127 -10
  112. package/translations/en-US.json +127 -10
  113. package/app/pages/checkout/partials/shipping-options.jsx +0 -269
@@ -0,0 +1,673 @@
1
+ /*
2
+ * Copyright (c) 2023, Salesforce, Inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+ import React from 'react'
8
+ import {render, screen, fireEvent, waitFor} from '@testing-library/react'
9
+ import {IntlProvider} from 'react-intl'
10
+ import {QueryClient, QueryClientProvider} from '@tanstack/react-query'
11
+ import ShippingAddress from '@salesforce/retail-react-app/app/pages/checkout/partials/shipping-address'
12
+ import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
13
+ import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
14
+ import {useCheckout} from '@salesforce/retail-react-app/app/pages/checkout/util/checkout-context'
15
+
16
+ // Mock the hooks
17
+ jest.mock('@salesforce/retail-react-app/app/pages/checkout/util/checkout-context')
18
+ jest.mock('@salesforce/retail-react-app/app/hooks/use-current-customer')
19
+ jest.mock('@salesforce/retail-react-app/app/hooks/use-current-basket')
20
+ jest.mock('@salesforce/retail-react-app/app/hooks/use-toast')
21
+
22
+ // Mock the new multiship and pickup hooks
23
+ jest.mock('@salesforce/retail-react-app/app/hooks/use-multiship')
24
+ jest.mock('@salesforce/retail-react-app/app/hooks/use-pickup-shipment')
25
+
26
+ // Mock the constants and getConfig with dynamic values for testing
27
+ let mockMultishipEnabled = true
28
+ let mockStoreLocatorEnabled = true
29
+
30
+ jest.mock('@salesforce/pwa-kit-runtime/utils/ssr-config', () => ({
31
+ getConfig: jest.fn(() => ({
32
+ app: {
33
+ multishipEnabled: mockMultishipEnabled,
34
+ storeLocatorEnabled: mockStoreLocatorEnabled
35
+ }
36
+ }))
37
+ }))
38
+
39
+ jest.mock('@salesforce/retail-react-app/app/constants', () => ({
40
+ get DEFAULT_SHIPMENT_ID() {
41
+ return 'me'
42
+ },
43
+ get STORE_LOCATOR_IS_ENABLED() {
44
+ return mockStoreLocatorEnabled
45
+ }
46
+ }))
47
+
48
+ // Helper function to set multishipEnabled for tests
49
+ const setMultishipEnabled = (enabled) => {
50
+ mockMultishipEnabled = enabled
51
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
52
+ const {getConfig} = require('@salesforce/pwa-kit-runtime/utils/ssr-config')
53
+ getConfig.mockReturnValue({
54
+ app: {
55
+ multishipEnabled: enabled,
56
+ storeLocatorEnabled: mockStoreLocatorEnabled
57
+ }
58
+ })
59
+ }
60
+
61
+ // Helper function to set storeLocatorEnabled for tests
62
+ const setStoreLocatorEnabled = (enabled) => {
63
+ mockStoreLocatorEnabled = enabled
64
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
65
+ const {getConfig} = require('@salesforce/pwa-kit-runtime/utils/ssr-config')
66
+ getConfig.mockReturnValue({
67
+ app: {
68
+ multishipEnabled: mockMultishipEnabled,
69
+ storeLocatorEnabled: enabled
70
+ }
71
+ })
72
+ }
73
+
74
+ // Mock mutation hooks to prevent QueryClient errors
75
+ const mockMutateAsync = jest.fn().mockResolvedValue({})
76
+ const mockCustomerMutateAsync = jest.fn().mockResolvedValue({})
77
+
78
+ jest.mock('@salesforce/commerce-sdk-react', () => {
79
+ const originalModule = jest.requireActual('@salesforce/commerce-sdk-react')
80
+ return {
81
+ ...originalModule,
82
+ useShopperCustomersMutation: () => ({
83
+ mutateAsync: mockCustomerMutateAsync
84
+ }),
85
+ useShopperBasketsMutation: () => ({
86
+ mutateAsync: mockMutateAsync
87
+ })
88
+ }
89
+ })
90
+
91
+ // Mock the toggle card components
92
+ jest.mock('@salesforce/retail-react-app/app/components/toggle-card', () => {
93
+ // eslint-disable-next-line react/prop-types
94
+ const ToggleCard = ({children, editing, onEdit, editLabel, editAction, onEditActionClick}) => (
95
+ <div data-testid="toggle-card" data-editing={editing ? 'true' : 'false'}>
96
+ {!editing && (
97
+ <button data-testid="edit-button" onClick={onEdit}>
98
+ {editLabel}
99
+ </button>
100
+ )}
101
+ {editing && editAction && onEditActionClick && (
102
+ <button data-testid="edit-action-button" onClick={onEditActionClick}>
103
+ {editAction}
104
+ </button>
105
+ )}
106
+ {children}
107
+ </div>
108
+ )
109
+
110
+ // eslint-disable-next-line react/prop-types
111
+ const ToggleCardEdit = ({children}) => <div data-testid="toggle-card-edit">{children}</div>
112
+
113
+ // eslint-disable-next-line react/prop-types
114
+ const ToggleCardSummary = ({children}) => (
115
+ <div data-testid="toggle-card-summary">{children}</div>
116
+ )
117
+
118
+ return {
119
+ ToggleCard,
120
+ ToggleCardEdit,
121
+ ToggleCardSummary
122
+ }
123
+ })
124
+
125
+ // Mock the shipping address selection component
126
+ jest.mock(
127
+ '@salesforce/retail-react-app/app/pages/checkout/partials/shipping-address-selection',
128
+ () => {
129
+ // eslint-disable-next-line react/prop-types
130
+ function MockShippingAddressSelection({onSubmit}) {
131
+ const mockAddress = {
132
+ addressId: 'addr-1',
133
+ address1: '123 Test St',
134
+ city: 'Test City',
135
+ countryCode: 'US',
136
+ firstName: 'John',
137
+ lastName: 'Doe',
138
+ phone: '555-555-5555',
139
+ postalCode: '12345',
140
+ stateCode: 'CA'
141
+ }
142
+ return (
143
+ <div data-testid="shipping-address-selection" role="button" tabIndex={0}>
144
+ Mock Shipping Address Selection
145
+ <button data-testid="submit-address" onClick={() => onSubmit(mockAddress)}>
146
+ Submit Address
147
+ </button>
148
+ </div>
149
+ )
150
+ }
151
+ return MockShippingAddressSelection
152
+ }
153
+ )
154
+
155
+ // Mock the multi-shipping component
156
+ jest.mock(
157
+ '@salesforce/retail-react-app/app/pages/checkout/partials/shipping-multi-address',
158
+ () => ({
159
+ __esModule: true,
160
+ default: function MockMultiShipping() {
161
+ const {
162
+ useCheckout
163
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
164
+ } = require('@salesforce/retail-react-app/app/pages/checkout/util/checkout-context')
165
+
166
+ const {goToStep, STEPS} = useCheckout()
167
+
168
+ return (
169
+ <div data-testid="multi-shipping" role="button" tabIndex={0}>
170
+ Mock Multi Shipping
171
+ <button
172
+ data-testid="submit-multi-address"
173
+ onClick={() => {
174
+ goToStep(STEPS.SHIPPING_OPTIONS)
175
+ }}
176
+ >
177
+ Submit Multi Address
178
+ </button>
179
+ </div>
180
+ )
181
+ }
182
+ })
183
+ )
184
+
185
+ const mockCustomer = {
186
+ customerId: 'customer-1',
187
+ isRegistered: true,
188
+ addresses: [
189
+ {
190
+ addressId: 'addr-1',
191
+ firstName: 'John',
192
+ lastName: 'Doe',
193
+ address1: '123 Test St',
194
+ city: 'Test City',
195
+ stateCode: 'CA',
196
+ postalCode: '12345'
197
+ },
198
+ {
199
+ addressId: 'addr-2',
200
+ firstName: 'Jane',
201
+ lastName: 'Smith',
202
+ address1: '456 Another St',
203
+ city: 'Another City',
204
+ stateCode: 'NY',
205
+ postalCode: '67890'
206
+ }
207
+ ]
208
+ }
209
+
210
+ const mockBasket = {
211
+ basketId: 'basket-1',
212
+ productItems: [
213
+ {
214
+ productId: 'product-1',
215
+ productName: 'Test Product 1',
216
+ quantity: 2,
217
+ itemId: 'item-1',
218
+ shipmentId: 'me'
219
+ },
220
+ {
221
+ productId: 'product-2',
222
+ productName: 'Test Product 2',
223
+ quantity: 1,
224
+ itemId: 'item-2',
225
+ shipmentId: 'me'
226
+ }
227
+ ],
228
+ shipments: [
229
+ {
230
+ shipmentId: 'me',
231
+ shippingMethod: {
232
+ id: 'standard-shipping',
233
+ c_storePickupEnabled: false
234
+ },
235
+ shippingAddress: {
236
+ address1: '123 Test St',
237
+ city: 'Test City',
238
+ stateCode: 'CA',
239
+ postalCode: '12345',
240
+ countryCode: 'US',
241
+ firstName: 'John',
242
+ lastName: 'Doe'
243
+ }
244
+ }
245
+ ]
246
+ }
247
+
248
+ const mockShowToast = jest.fn()
249
+
250
+ const mockCheckoutContext = {
251
+ step: 3, // SHIPPING_ADDRESS
252
+ goToStep: jest.fn(),
253
+ STEPS: {
254
+ SHIPPING_ADDRESS: 3,
255
+ SHIPPING_OPTIONS: 4,
256
+ PAYMENT: 5,
257
+ REVIEW_ORDER: 6
258
+ }
259
+ }
260
+
261
+ const defaultProps = {
262
+ basket: mockBasket,
263
+ customer: mockCustomer
264
+ }
265
+
266
+ const renderWithIntl = (component) => {
267
+ const queryClient = new QueryClient({
268
+ defaultOptions: {
269
+ queries: {
270
+ retry: false
271
+ }
272
+ }
273
+ })
274
+ return render(
275
+ <QueryClientProvider client={queryClient}>
276
+ <IntlProvider locale="en">{component}</IntlProvider>
277
+ </QueryClientProvider>
278
+ )
279
+ }
280
+
281
+ describe('ShippingAddress', () => {
282
+ beforeEach(() => {
283
+ // Reset multishipEnabled to default value for test isolation
284
+ setMultishipEnabled(true)
285
+ setStoreLocatorEnabled(true)
286
+
287
+ mockCheckoutContext.goToStep.mockClear()
288
+ mockShowToast.mockClear()
289
+ mockMutateAsync.mockClear()
290
+ mockCustomerMutateAsync.mockClear()
291
+ useCurrentCustomer.mockReturnValue({
292
+ data: mockCustomer
293
+ })
294
+ useCurrentBasket.mockReturnValue({
295
+ data: mockBasket
296
+ })
297
+ useCheckout.mockReturnValue(mockCheckoutContext)
298
+
299
+ // Mock useMultiship hook
300
+ const useMultiship =
301
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
302
+ require('@salesforce/retail-react-app/app/hooks/use-multiship').useMultiship
303
+ useMultiship.mockReturnValue({
304
+ findExistingDeliveryShipment: jest.fn().mockReturnValue(mockBasket.shipments[0]),
305
+ moveItemsToDeliveryShipment: jest.fn().mockResolvedValue(mockBasket),
306
+ removeEmptyShipments: jest.fn().mockResolvedValue()
307
+ })
308
+
309
+ // Mock useToast hook
310
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
311
+ const useToast = require('@salesforce/retail-react-app/app/hooks/use-toast').useToast
312
+ useToast.mockReturnValue(mockShowToast)
313
+ })
314
+
315
+ afterEach(() => {
316
+ jest.clearAllMocks()
317
+ // Reset to default values
318
+ mockMultishipEnabled = true
319
+ mockStoreLocatorEnabled = true
320
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
321
+ const {getConfig} = require('@salesforce/pwa-kit-runtime/utils/ssr-config')
322
+ getConfig.mockReturnValue({
323
+ app: {
324
+ multishipEnabled: mockMultishipEnabled,
325
+ storeLocatorEnabled: mockStoreLocatorEnabled
326
+ }
327
+ })
328
+ })
329
+
330
+ it('should render shipping address selection for single shipping', () => {
331
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
332
+
333
+ expect(screen.getByTestId('shipping-address-selection')).toBeInTheDocument()
334
+ expect(screen.queryByTestId('multi-shipping')).not.toBeInTheDocument()
335
+ })
336
+
337
+ it('should render multi-shipping when multiple items and toggle is enabled', () => {
338
+ // Mock that we're in editing mode and have multiple items
339
+ const editingContext = {
340
+ ...mockCheckoutContext,
341
+ step: 3 // SHIPPING_ADDRESS
342
+ }
343
+ useCheckout.mockReturnValue(editingContext)
344
+
345
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
346
+
347
+ // Should show shipping address selection by default
348
+ expect(screen.getByTestId('shipping-address-selection')).toBeInTheDocument()
349
+ // Multi-shipping is not shown by default, only when toggled
350
+ expect(screen.queryByTestId('multi-shipping')).not.toBeInTheDocument()
351
+ })
352
+
353
+ it('should handle single shipping submission', async () => {
354
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
355
+
356
+ fireEvent.click(screen.getByTestId('submit-address'))
357
+
358
+ // Should navigate to shipping options step
359
+ await waitFor(() => {
360
+ expect(mockCheckoutContext.goToStep).toHaveBeenCalledWith(4) // SHIPPING_OPTIONS
361
+ })
362
+ })
363
+
364
+ it('should handle multi-shipping submission', async () => {
365
+ // Mock that we're in editing mode
366
+ const editingContext = {
367
+ ...mockCheckoutContext,
368
+ step: 3 // SHIPPING_ADDRESS
369
+ }
370
+ useCheckout.mockReturnValue(editingContext)
371
+
372
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
373
+
374
+ // First click the edit action button to enable multi-shipping
375
+ fireEvent.click(screen.getByTestId('edit-action-button'))
376
+
377
+ // Now the multi-shipping component should be visible
378
+ expect(screen.getByTestId('multi-shipping')).toBeInTheDocument()
379
+
380
+ fireEvent.click(screen.getByTestId('submit-multi-address'))
381
+
382
+ // Should navigate to shipping options step
383
+ await waitFor(() => {
384
+ expect(mockCheckoutContext.goToStep).toHaveBeenCalledWith(4) // SHIPPING_OPTIONS
385
+ })
386
+ })
387
+
388
+ it('should show edit button with correct label for single shipping', () => {
389
+ // Mock that we're NOT in editing mode to get "Edit Shipping Address" label
390
+ const summaryContext = {
391
+ ...mockCheckoutContext,
392
+ step: 4 // SHIPPING_OPTIONS (not editing)
393
+ }
394
+ useCheckout.mockReturnValue(summaryContext)
395
+
396
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
397
+
398
+ const editButton = screen.getByTestId('edit-button')
399
+ expect(editButton).toBeInTheDocument()
400
+ expect(editButton).toHaveTextContent('Edit Shipping Address')
401
+ })
402
+
403
+ it('should show edit action button with correct label for multi-shipping when enabled', () => {
404
+ // Mock that we're in editing mode with multiple items
405
+ const editingContext = {
406
+ ...mockCheckoutContext,
407
+ step: 3 // SHIPPING_ADDRESS
408
+ }
409
+ useCheckout.mockReturnValue(editingContext)
410
+
411
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
412
+
413
+ const editActionButton = screen.getByTestId('edit-action-button')
414
+ expect(editActionButton).toBeInTheDocument()
415
+ expect(editActionButton).toHaveTextContent('Ship to Multiple Addresses')
416
+ })
417
+
418
+ it('should handle edit button click for single shipping', () => {
419
+ // Mock that we're NOT in editing mode
420
+ const summaryContext = {
421
+ ...mockCheckoutContext,
422
+ step: 4 // SHIPPING_OPTIONS (not editing)
423
+ }
424
+ useCheckout.mockReturnValue(summaryContext)
425
+
426
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
427
+
428
+ fireEvent.click(screen.getByTestId('edit-button'))
429
+
430
+ // Should navigate to shipping address step
431
+ expect(mockCheckoutContext.goToStep).toHaveBeenCalledWith(3) // SHIPPING_ADDRESS
432
+ })
433
+
434
+ it('should handle edit action button click for multi-shipping', () => {
435
+ // Mock that we're in editing mode
436
+ const editingContext = {
437
+ ...mockCheckoutContext,
438
+ step: 3 // SHIPPING_ADDRESS
439
+ }
440
+ useCheckout.mockReturnValue(editingContext)
441
+
442
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
443
+
444
+ fireEvent.click(screen.getByTestId('edit-action-button'))
445
+
446
+ // Should enable multi-shipping mode
447
+ expect(screen.getByTestId('multi-shipping')).toBeInTheDocument()
448
+ })
449
+
450
+ it('should handle empty basket gracefully', () => {
451
+ const emptyBasket = {...mockBasket, productItems: []}
452
+
453
+ renderWithIntl(<ShippingAddress {...defaultProps} basket={emptyBasket} />)
454
+
455
+ // Should still render the component
456
+ expect(screen.getByTestId('toggle-card')).toBeInTheDocument()
457
+ })
458
+
459
+ it('should handle missing customer data gracefully', () => {
460
+ useCurrentCustomer.mockReturnValue({
461
+ data: null
462
+ })
463
+
464
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
465
+
466
+ // Should still render the component
467
+ expect(screen.getByTestId('toggle-card')).toBeInTheDocument()
468
+ })
469
+
470
+ it('should handle missing basket data gracefully', () => {
471
+ useCurrentBasket.mockReturnValue({
472
+ data: null
473
+ })
474
+
475
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
476
+
477
+ // Should still render the component
478
+ expect(screen.getByTestId('toggle-card')).toBeInTheDocument()
479
+ })
480
+
481
+ it('should show toast error when address submission fails', async () => {
482
+ // Mock the mutation to throw an error
483
+ mockMutateAsync.mockRejectedValueOnce(new Error('Network error'))
484
+
485
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
486
+
487
+ // Submit the address form
488
+ fireEvent.click(screen.getByTestId('submit-address'))
489
+
490
+ // Wait for the error to be handled
491
+ await waitFor(() => {
492
+ expect(mockShowToast).toHaveBeenCalledWith({
493
+ title: 'Something went wrong while updating the shipping address. Try again.',
494
+ status: 'error'
495
+ })
496
+ })
497
+
498
+ // Verify that goToStep was not called due to the error
499
+ expect(mockCheckoutContext.goToStep).not.toHaveBeenCalled()
500
+ })
501
+
502
+ describe('Toggle Card Behavior', () => {
503
+ it('should show edit mode when in shipping address step', () => {
504
+ const editingContext = {
505
+ ...mockCheckoutContext,
506
+ step: 3 // SHIPPING_ADDRESS
507
+ }
508
+ useCheckout.mockReturnValue(editingContext)
509
+
510
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
511
+
512
+ const toggleCard = screen.getByTestId('toggle-card')
513
+ expect(toggleCard).toHaveAttribute('data-editing', 'true')
514
+ })
515
+
516
+ it('should show summary mode when not in shipping address step', () => {
517
+ const summaryContext = {
518
+ ...mockCheckoutContext,
519
+ step: 4 // SHIPPING_OPTIONS
520
+ }
521
+ useCheckout.mockReturnValue(summaryContext)
522
+
523
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
524
+
525
+ const toggleCard = screen.getByTestId('toggle-card')
526
+ expect(toggleCard).toHaveAttribute('data-editing', 'false')
527
+ })
528
+ })
529
+
530
+ describe('Multi-shipping Toggle', () => {
531
+ it('should show multi-shipping option when multiple items exist', () => {
532
+ const editingContext = {
533
+ ...mockCheckoutContext,
534
+ step: 3 // SHIPPING_ADDRESS
535
+ }
536
+ useCheckout.mockReturnValue(editingContext)
537
+
538
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
539
+
540
+ // Should show shipping address selection by default
541
+ expect(screen.getByTestId('shipping-address-selection')).toBeInTheDocument()
542
+ // Multi-shipping is not shown by default, only when toggled
543
+ expect(screen.queryByTestId('multi-shipping')).not.toBeInTheDocument()
544
+
545
+ // Click edit action button to enable multi-shipping
546
+ fireEvent.click(screen.getByTestId('edit-action-button'))
547
+
548
+ // Now multi-shipping should be visible
549
+ expect(screen.getByTestId('multi-shipping')).toBeInTheDocument()
550
+ })
551
+
552
+ it('should not show multi-shipping option when only one item exists', () => {
553
+ const singleItemBasket = {
554
+ ...mockBasket,
555
+ productItems: [mockBasket.productItems[0]]
556
+ }
557
+ const editingContext = {
558
+ ...mockCheckoutContext,
559
+ step: 3 // SHIPPING_ADDRESS
560
+ }
561
+ useCheckout.mockReturnValue(editingContext)
562
+
563
+ renderWithIntl(<ShippingAddress {...defaultProps} basket={singleItemBasket} />)
564
+
565
+ // Should show shipping address selection
566
+ expect(screen.getByTestId('shipping-address-selection')).toBeInTheDocument()
567
+ // Multi-shipping should not be available for single item
568
+ expect(screen.queryByTestId('multi-shipping')).not.toBeInTheDocument()
569
+ })
570
+ })
571
+
572
+ describe('multishipEnabled behavior', () => {
573
+ describe('when multishipEnabled is true', () => {
574
+ beforeEach(() => {
575
+ setMultishipEnabled(true)
576
+ })
577
+
578
+ it('should show "Ship to Multiple Addresses" button when multishipEnabled is true', () => {
579
+ // Mock that we're in editing mode with multiple items
580
+ const editingContext = {
581
+ ...mockCheckoutContext,
582
+ step: 3 // SHIPPING_ADDRESS
583
+ }
584
+ useCheckout.mockReturnValue(editingContext)
585
+
586
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
587
+
588
+ const editActionButton = screen.getByTestId('edit-action-button')
589
+ expect(editActionButton).toBeInTheDocument()
590
+ expect(editActionButton).toHaveTextContent('Ship to Multiple Addresses')
591
+ })
592
+
593
+ it('should handle "Ship to Multiple Addresses" button click to toggle multi-shipping', () => {
594
+ // Mock that we're in editing mode
595
+ const editingContext = {
596
+ ...mockCheckoutContext,
597
+ step: 3 // SHIPPING_ADDRESS
598
+ }
599
+ useCheckout.mockReturnValue(editingContext)
600
+
601
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
602
+
603
+ const editActionButton = screen.getByTestId('edit-action-button')
604
+ fireEvent.click(editActionButton)
605
+ expect(screen.getByTestId('multi-shipping')).toBeInTheDocument()
606
+ })
607
+ })
608
+
609
+ describe('when multishipEnabled is false', () => {
610
+ beforeEach(() => {
611
+ setMultishipEnabled(false)
612
+ })
613
+
614
+ it('should not show "Ship to Multiple Addresses" button when multishipEnabled is false', () => {
615
+ const editingContext = {
616
+ ...mockCheckoutContext,
617
+ step: 3 // SHIPPING_ADDRESS
618
+ }
619
+ useCheckout.mockReturnValue(editingContext)
620
+
621
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
622
+
623
+ expect(screen.queryByTestId('edit-action-button')).not.toBeInTheDocument()
624
+ })
625
+
626
+ it('should still show shipping address selection when multishipEnabled is false', () => {
627
+ // Mock that we're in editing mode
628
+ const editingContext = {
629
+ ...mockCheckoutContext,
630
+ step: 3 // SHIPPING_ADDRESS
631
+ }
632
+ useCheckout.mockReturnValue(editingContext)
633
+
634
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
635
+
636
+ // Should still show shipping address selection
637
+ expect(screen.getByTestId('shipping-address-selection')).toBeInTheDocument()
638
+ // But no "Ship to Multiple Addresses" button
639
+ expect(screen.queryByTestId('edit-action-button')).not.toBeInTheDocument()
640
+ })
641
+ })
642
+
643
+ describe('common behavior regardless of multishipEnabled setting', () => {
644
+ it('should show edit mode when in shipping address step', () => {
645
+ setMultishipEnabled(true) // Test with enabled
646
+ const editingContext = {
647
+ ...mockCheckoutContext,
648
+ step: 3 // SHIPPING_ADDRESS
649
+ }
650
+ useCheckout.mockReturnValue(editingContext)
651
+
652
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
653
+
654
+ const toggleCard = screen.getByTestId('toggle-card')
655
+ expect(toggleCard).toHaveAttribute('data-editing', 'true')
656
+ })
657
+
658
+ it('should show summary mode when not in shipping address step', () => {
659
+ setMultishipEnabled(false) // Test with disabled
660
+ const summaryContext = {
661
+ ...mockCheckoutContext,
662
+ step: 4 // SHIPPING_OPTIONS
663
+ }
664
+ useCheckout.mockReturnValue(summaryContext)
665
+
666
+ renderWithIntl(<ShippingAddress {...defaultProps} />)
667
+
668
+ const toggleCard = screen.getByTestId('toggle-card')
669
+ expect(toggleCard).toHaveAttribute('data-editing', 'false')
670
+ })
671
+ })
672
+ })
673
+ })