@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,472 @@
1
+ /*
2
+ * Copyright (c) 2021, salesforce.com, 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, waitFor} from '@testing-library/react'
9
+ import {IntlProvider} from 'react-intl'
10
+ import {CurrencyProvider} from '@salesforce/retail-react-app/app/contexts'
11
+ import ShippingMethods from '@salesforce/retail-react-app/app/pages/checkout/partials/shipping-methods'
12
+ import {useCheckout} from '@salesforce/retail-react-app/app/pages/checkout/util/checkout-context'
13
+ import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
14
+ import {useCurrency} from '@salesforce/retail-react-app/app/hooks'
15
+ import {useShippingMethodsForShipment, useProducts} from '@salesforce/commerce-sdk-react'
16
+
17
+ // Mock the hooks
18
+ jest.mock('@salesforce/retail-react-app/app/pages/checkout/util/checkout-context')
19
+ jest.mock('@salesforce/retail-react-app/app/hooks/use-current-basket')
20
+ jest.mock('@salesforce/retail-react-app/app/hooks')
21
+ jest.mock('@salesforce/commerce-sdk-react')
22
+
23
+ const mockUseCheckout = useCheckout
24
+ const mockUseCurrentBasket = useCurrentBasket
25
+ const mockUseCurrency = useCurrency
26
+ const mockUseShippingMethodsForShipment = useShippingMethodsForShipment
27
+ const mockUseProducts = useProducts
28
+
29
+ // Mock data
30
+ const mockBasket = {
31
+ basketId: 'basket-1',
32
+ shipments: [
33
+ {
34
+ shipmentId: 'shipment-1',
35
+ shippingAddress: {
36
+ firstName: 'John',
37
+ lastName: 'Doe',
38
+ address1: '123 Main St',
39
+ city: 'Anytown',
40
+ stateCode: 'CA',
41
+ postalCode: '12345'
42
+ },
43
+ shippingMethod: {
44
+ id: 'shipping-method-1',
45
+ name: 'Standard Shipping',
46
+ description: '5-7 business days'
47
+ }
48
+ }
49
+ ],
50
+ productItems: [
51
+ {
52
+ itemId: 'item-1',
53
+ productId: 'product-1',
54
+ productName: 'Test Product 1',
55
+ quantity: 2,
56
+ price: 29.99,
57
+ variationValues: {
58
+ color: 'Red',
59
+ size: 'Medium'
60
+ }
61
+ },
62
+ {
63
+ itemId: 'item-2',
64
+ productId: 'product-2',
65
+ productName: 'Test Product 2',
66
+ quantity: 1,
67
+ price: 19.99,
68
+ variationValues: {
69
+ color: 'Blue',
70
+ size: 'Large'
71
+ }
72
+ }
73
+ ],
74
+ shippingItems: [
75
+ {
76
+ shipmentId: 'shipment-1',
77
+ price: 5.99,
78
+ priceAfterItemDiscount: 5.99
79
+ }
80
+ ]
81
+ }
82
+
83
+ const mockShippingMethods = {
84
+ applicableShippingMethods: [
85
+ {
86
+ id: 'shipping-method-1',
87
+ name: 'Standard Shipping',
88
+ description: '5-7 business days',
89
+ price: 5.99
90
+ },
91
+ {
92
+ id: 'shipping-method-2',
93
+ name: 'Express Shipping',
94
+ description: '2-3 business days',
95
+ price: 12.99
96
+ }
97
+ ],
98
+ defaultShippingMethodId: 'shipping-method-1'
99
+ }
100
+
101
+ const mockProductsMap = {
102
+ 'product-1': {
103
+ id: 'product-1',
104
+ name: 'Test Product 1',
105
+ imageGroups: [
106
+ {
107
+ viewType: 'small',
108
+ images: [
109
+ {
110
+ link: 'https://test-image-1.jpg',
111
+ disBaseLink: 'https://test-image-1.jpg'
112
+ }
113
+ ]
114
+ }
115
+ ]
116
+ },
117
+ 'product-2': {
118
+ id: 'product-2',
119
+ name: 'Test Product 2',
120
+ imageGroups: [
121
+ {
122
+ viewType: 'small',
123
+ images: [
124
+ {
125
+ link: 'https://test-image-2.jpg',
126
+ disBaseLink: 'https://test-image-2.jpg'
127
+ }
128
+ ]
129
+ }
130
+ ]
131
+ }
132
+ }
133
+
134
+ const defaultProps = {
135
+ step: 'SHIPPING_OPTIONS',
136
+ STEPS: {
137
+ SHIPPING_OPTIONS: 'SHIPPING_OPTIONS'
138
+ },
139
+ goToStep: jest.fn(),
140
+ goToNextStep: jest.fn()
141
+ }
142
+
143
+ const renderWithIntl = (component) => {
144
+ return render(
145
+ <CurrencyProvider currency="USD">
146
+ <IntlProvider locale="en">{component}</IntlProvider>
147
+ </CurrencyProvider>
148
+ )
149
+ }
150
+
151
+ describe('ShippingMethods', () => {
152
+ beforeEach(() => {
153
+ mockUseCheckout.mockReturnValue(defaultProps)
154
+ mockUseCurrentBasket.mockReturnValue({
155
+ data: mockBasket,
156
+ derivedData: {
157
+ totalShippingCost: 5.99
158
+ },
159
+ isLoading: false
160
+ })
161
+ mockUseCurrency.mockReturnValue({
162
+ currency: 'USD'
163
+ })
164
+ mockUseShippingMethodsForShipment.mockReturnValue({
165
+ data: mockShippingMethods,
166
+ isLoading: false
167
+ })
168
+ mockUseProducts.mockReturnValue({
169
+ data: mockProductsMap,
170
+ isLoading: false
171
+ })
172
+ })
173
+
174
+ afterEach(() => {
175
+ jest.clearAllMocks()
176
+ })
177
+
178
+ describe('Loading States', () => {
179
+ test('should show loading spinner when basket is loading', () => {
180
+ mockUseCurrentBasket.mockReturnValue({
181
+ data: null,
182
+ derivedData: {
183
+ totalShippingCost: undefined
184
+ },
185
+ isLoading: true
186
+ })
187
+
188
+ renderWithIntl(<ShippingMethods />)
189
+
190
+ expect(screen.getAllByTestId('loading').length).toBeGreaterThan(0)
191
+ })
192
+
193
+ test('should show loading spinner for shipping methods when shipping methods are loading', async () => {
194
+ mockUseShippingMethodsForShipment.mockReturnValue({
195
+ data: null,
196
+ isLoading: true
197
+ })
198
+
199
+ renderWithIntl(<ShippingMethods />)
200
+
201
+ // Wait for the loading spinner to appear
202
+ await waitFor(() => {
203
+ expect(screen.getAllByTestId('loading').length).toBeGreaterThan(0)
204
+ })
205
+ })
206
+
207
+ test('should show loading spinner when multiple data sources are loading', () => {
208
+ mockUseCurrentBasket.mockReturnValue({
209
+ data: null,
210
+ derivedData: {
211
+ totalShippingCost: undefined
212
+ },
213
+ isLoading: true
214
+ })
215
+ mockUseProducts.mockReturnValue({
216
+ data: {},
217
+ isLoading: true
218
+ })
219
+ mockUseShippingMethodsForShipment.mockReturnValue({
220
+ data: null,
221
+ isLoading: true
222
+ })
223
+
224
+ renderWithIntl(<ShippingMethods />)
225
+
226
+ expect(screen.getAllByTestId('loading').length).toBeGreaterThan(0)
227
+ })
228
+ })
229
+
230
+ describe('Component Rendering', () => {
231
+ test('should render shipping options when all data is loaded', () => {
232
+ renderWithIntl(<ShippingMethods />)
233
+
234
+ expect(screen.getByText('Shipping & Gift Options')).toBeInTheDocument()
235
+ expect(screen.getByText('Standard Shipping')).toBeInTheDocument()
236
+ expect(screen.getByText('Express Shipping')).toBeInTheDocument()
237
+ })
238
+
239
+ test('should render shipping methods correctly', () => {
240
+ renderWithIntl(<ShippingMethods />)
241
+
242
+ expect(screen.getByText('Standard Shipping')).toBeInTheDocument()
243
+ expect(screen.getByText('Express Shipping')).toBeInTheDocument()
244
+ expect(screen.getByText('5-7 business days')).toBeInTheDocument()
245
+ expect(screen.getByText('2-3 business days')).toBeInTheDocument()
246
+ })
247
+
248
+ test('should render shipping methods when shipping methods are loaded', () => {
249
+ renderWithIntl(<ShippingMethods />)
250
+
251
+ expect(screen.getByText('Standard Shipping')).toBeInTheDocument()
252
+ expect(screen.getByText('Express Shipping')).toBeInTheDocument()
253
+ expect(screen.getByText('5-7 business days')).toBeInTheDocument()
254
+ expect(screen.getByText('2-3 business days')).toBeInTheDocument()
255
+ })
256
+
257
+ test('should display shipping cost from derivedData correctly', () => {
258
+ renderWithIntl(<ShippingMethods />)
259
+ expect(screen.getByText('$5.99')).toBeInTheDocument()
260
+ })
261
+ })
262
+
263
+ describe('Form Functionality', () => {
264
+ test('should render continue button when shipping method is selected', () => {
265
+ renderWithIntl(<ShippingMethods />)
266
+
267
+ const continueButton = screen.getByText('Continue to Payment')
268
+ expect(continueButton).toBeInTheDocument()
269
+ expect(continueButton).not.toBeDisabled()
270
+ })
271
+
272
+ test('should disable continue button when no shipping method is selected', () => {
273
+ // Mock basket without shipping method
274
+ const basketWithoutShippingMethod = {
275
+ ...mockBasket,
276
+ shipments: [
277
+ {
278
+ ...mockBasket.shipments[0],
279
+ shippingMethod: null
280
+ }
281
+ ]
282
+ }
283
+
284
+ mockUseCurrentBasket.mockReturnValue({
285
+ data: basketWithoutShippingMethod,
286
+ derivedData: {
287
+ totalShippingCost: 5.99
288
+ },
289
+ isLoading: false
290
+ })
291
+
292
+ renderWithIntl(<ShippingMethods />)
293
+
294
+ const continueButton = screen.getByText('Continue to Payment')
295
+ expect(continueButton).toBeDisabled()
296
+ })
297
+ })
298
+
299
+ describe('Multiple Shipments', () => {
300
+ test('should render multiple shipments correctly', () => {
301
+ const multiShipmentBasket = {
302
+ ...mockBasket,
303
+ shipments: [
304
+ {
305
+ shipmentId: 'shipment-1',
306
+ shippingAddress: {
307
+ firstName: 'John',
308
+ lastName: 'Doe',
309
+ address1: '123 Main St',
310
+ city: 'Anytown',
311
+ stateCode: 'CA',
312
+ postalCode: '12345'
313
+ },
314
+ shippingMethod: {
315
+ id: 'shipping-method-1',
316
+ name: 'Standard Shipping',
317
+ description: '5-7 business days'
318
+ }
319
+ },
320
+ {
321
+ shipmentId: 'shipment-2',
322
+ shippingAddress: {
323
+ firstName: 'Jane',
324
+ lastName: 'Smith',
325
+ address1: '456 Oak Ave',
326
+ city: 'Somewhere',
327
+ stateCode: 'NY',
328
+ postalCode: '67890'
329
+ },
330
+ shippingMethod: {
331
+ id: 'shipping-method-2',
332
+ name: 'Express Shipping',
333
+ description: '2-3 business days'
334
+ }
335
+ }
336
+ ]
337
+ }
338
+
339
+ mockUseCurrentBasket.mockReturnValue({
340
+ data: multiShipmentBasket,
341
+ derivedData: {
342
+ totalShippingCost: 18.98
343
+ },
344
+ isLoading: false
345
+ })
346
+
347
+ renderWithIntl(<ShippingMethods />)
348
+
349
+ expect(screen.getAllByText('Standard Shipping').length).toBeGreaterThan(0)
350
+ expect(screen.getAllByText('Express Shipping').length).toBeGreaterThan(0)
351
+ })
352
+ })
353
+
354
+ describe('Error Handling', () => {
355
+ test('should handle missing basket data gracefully', () => {
356
+ mockUseCurrentBasket.mockReturnValue({
357
+ data: null,
358
+ derivedData: {
359
+ totalShippingCost: undefined
360
+ },
361
+ isLoading: false
362
+ })
363
+
364
+ renderWithIntl(<ShippingMethods />)
365
+
366
+ // Should not crash and should show appropriate state
367
+ expect(screen.getByText('Shipping & Gift Options')).toBeInTheDocument()
368
+ })
369
+
370
+ test('should handle missing shipping address gracefully', () => {
371
+ const basketWithoutAddress = {
372
+ ...mockBasket,
373
+ shipments: [
374
+ {
375
+ ...mockBasket.shipments[0],
376
+ shippingAddress: null
377
+ }
378
+ ]
379
+ }
380
+
381
+ mockUseCurrentBasket.mockReturnValue({
382
+ data: basketWithoutAddress,
383
+ derivedData: {
384
+ totalShippingCost: 5.99
385
+ },
386
+ isLoading: false
387
+ })
388
+
389
+ renderWithIntl(<ShippingMethods />)
390
+
391
+ // Should not crash and should show appropriate state
392
+ expect(screen.getByText('Shipping & Gift Options')).toBeInTheDocument()
393
+ })
394
+ })
395
+
396
+ describe('Product Display', () => {
397
+ test('should render shipping options component with basic structure', () => {
398
+ renderWithIntl(<ShippingMethods />)
399
+
400
+ // Check that the main component structure is rendered
401
+ expect(screen.getByText('Shipping & Gift Options')).toBeInTheDocument()
402
+ expect(screen.getByText('Continue to Payment')).toBeInTheDocument()
403
+ })
404
+ })
405
+
406
+ describe('Shipping Promotions', () => {
407
+ test('should display shipping promotions when available', () => {
408
+ const shippingMethodsWithPromotions = {
409
+ ...mockShippingMethods,
410
+ applicableShippingMethods: [
411
+ {
412
+ ...mockShippingMethods.applicableShippingMethods[0],
413
+ shippingPromotions: [
414
+ {
415
+ promotionId: 'promo-1',
416
+ calloutMsg: 'Free shipping on orders over $50'
417
+ }
418
+ ]
419
+ }
420
+ ]
421
+ }
422
+
423
+ mockUseShippingMethodsForShipment.mockReturnValue({
424
+ data: shippingMethodsWithPromotions,
425
+ isLoading: false
426
+ })
427
+
428
+ renderWithIntl(<ShippingMethods />)
429
+
430
+ expect(screen.getByText('Free shipping on orders over $50')).toBeInTheDocument()
431
+ })
432
+ })
433
+
434
+ describe('Loading State Transitions', () => {
435
+ test('should transition from loading to loaded state smoothly', async () => {
436
+ // Start with loading state
437
+ mockUseCurrentBasket.mockReturnValue({
438
+ data: null,
439
+ derivedData: {
440
+ totalShippingCost: undefined
441
+ },
442
+ isLoading: true
443
+ })
444
+
445
+ const {rerender} = renderWithIntl(<ShippingMethods />)
446
+ expect(screen.getAllByTestId('loading').length).toBeGreaterThan(0)
447
+
448
+ // Transition to loaded state
449
+ mockUseCurrentBasket.mockReturnValue({
450
+ data: mockBasket,
451
+ derivedData: {
452
+ totalShippingCost: 5.99
453
+ },
454
+ isLoading: false
455
+ })
456
+
457
+ rerender(
458
+ <CurrencyProvider currency="USD">
459
+ <IntlProvider locale="en">
460
+ <ShippingMethods />
461
+ </IntlProvider>
462
+ </CurrencyProvider>
463
+ )
464
+
465
+ await waitFor(() => {
466
+ expect(screen.queryAllByTestId('loading')).toHaveLength(0)
467
+ })
468
+
469
+ expect(screen.getByText('Standard Shipping')).toBeInTheDocument()
470
+ })
471
+ })
472
+ })