@salesforce/retail-react-app 7.1.0-preview.0 → 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.
- package/CHANGELOG.md +8 -4
- package/app/components/_app/index.jsx +9 -7
- package/app/components/_app/index.test.js +2 -2
- package/app/components/_app-config/index.jsx +9 -3
- package/app/components/drawer-menu/drawer-menu.jsx +3 -1
- package/app/components/footer/index.jsx +3 -1
- package/app/components/header/index.jsx +3 -1
- package/app/components/header/index.test.js +2 -2
- package/app/components/island/README.md +1 -1
- package/app/components/island/index.jsx +3 -1
- package/app/components/island/index.test.js +94 -5
- package/app/components/item-variant/item-attributes.jsx +12 -3
- package/app/components/multiship/multiship-order-summary.jsx +137 -0
- package/app/components/multiship/multiship-order-summary.test.js +121 -0
- package/app/components/order-summary/index.jsx +2 -4
- package/app/components/pickup-or-delivery/index.jsx +80 -0
- package/app/components/pickup-or-delivery/index.test.jsx +182 -0
- package/app/components/product-item/index.jsx +26 -16
- package/app/components/product-item/index.test.js +29 -2
- package/app/components/product-item-list/index.jsx +10 -0
- package/app/components/product-item-list/index.test.jsx +14 -0
- package/app/components/product-view/index.jsx +9 -6
- package/app/components/product-view/index.test.js +25 -21
- package/app/components/quantity-picker/index.test.jsx +12 -12
- package/app/components/reset-password/index.test.js +1 -1
- package/app/components/shared/ui/AlertDescription/index.jsx +8 -0
- package/app/components/shared/ui/index.jsx +1 -0
- package/app/components/store-display/index.jsx +28 -4
- package/app/components/store-display/index.test.js +71 -0
- package/app/components/store-locator/form.test.jsx +16 -4
- package/app/components/store-locator/list.jsx +9 -4
- package/app/components/toggle-card/index.jsx +14 -0
- package/app/components/unavailable-product-confirmation-modal/index.jsx +19 -5
- package/app/components/unavailable-product-confirmation-modal/index.test.js +122 -1
- package/app/constants.js +20 -6
- package/app/contexts/store-locator-provider.jsx +7 -1
- package/app/contexts/store-locator-provider.test.jsx +36 -1
- package/app/hooks/use-address-form.js +155 -0
- package/app/hooks/use-address-form.test.js +501 -0
- package/app/hooks/use-auth-modal.js +2 -6
- package/app/hooks/use-current-basket.js +71 -2
- package/app/hooks/use-current-basket.test.js +37 -1
- package/app/hooks/use-dnt-notification.js +4 -4
- package/app/hooks/use-dnt-notification.test.js +5 -5
- package/app/hooks/use-item-shipment-management.js +233 -0
- package/app/hooks/use-item-shipment-management.test.js +696 -0
- package/app/hooks/use-multiship.js +589 -0
- package/app/hooks/use-multiship.test.js +776 -0
- package/app/hooks/use-pickup-shipment.js +70 -106
- package/app/hooks/use-pickup-shipment.test.js +345 -209
- package/app/hooks/use-product-address-assignment.js +280 -0
- package/app/hooks/use-product-address-assignment.test.js +414 -0
- package/app/hooks/use-product-inventory.js +100 -0
- package/app/hooks/use-product-inventory.test.js +254 -0
- package/app/hooks/use-shipment-operations.js +168 -0
- package/app/hooks/use-shipment-operations.test.js +385 -0
- package/app/hooks/use-store-locator.js +24 -2
- package/app/hooks/use-store-locator.test.jsx +109 -1
- package/app/pages/account/index.test.js +1 -1
- package/app/pages/account/profile.test.js +0 -2
- package/app/pages/cart/index.jsx +397 -157
- package/app/pages/cart/index.test.js +353 -2
- package/app/pages/cart/partials/bonus-products-title.jsx +10 -8
- package/app/pages/cart/partials/cart-secondary-button-group.test.js +1 -1
- package/app/pages/cart/partials/order-type-display.jsx +68 -0
- package/app/pages/cart/partials/order-type-display.test.js +241 -0
- package/app/pages/checkout/confirmation.jsx +79 -158
- package/app/pages/checkout/index.jsx +34 -9
- package/app/pages/checkout/index.test.js +245 -118
- package/app/pages/checkout/partials/contact-info.jsx +2 -6
- package/app/pages/checkout/partials/contact-info.test.js +93 -7
- package/app/pages/checkout/partials/payment.jsx +19 -5
- package/app/pages/checkout/partials/pickup-address.jsx +340 -70
- package/app/pages/checkout/partials/pickup-address.test.js +1075 -82
- package/app/pages/checkout/partials/product-shipping-address-card.jsx +382 -0
- package/app/pages/checkout/partials/shipment-details.jsx +209 -0
- package/app/pages/checkout/partials/shipment-details.test.js +246 -0
- package/app/pages/checkout/partials/shipping-address.jsx +156 -68
- package/app/pages/checkout/partials/shipping-address.test.js +673 -0
- package/app/pages/checkout/partials/shipping-method-options.jsx +180 -0
- package/app/pages/checkout/partials/shipping-methods.jsx +403 -0
- package/app/pages/checkout/partials/shipping-methods.test.js +472 -0
- package/app/pages/checkout/partials/shipping-multi-address.jsx +259 -0
- package/app/pages/checkout/partials/shipping-multi-address.test.js +2088 -0
- package/app/pages/checkout/partials/shipping-product-cards.jsx +101 -0
- package/app/pages/checkout/util/checkout-context.js +25 -18
- package/app/pages/login/index.jsx +2 -6
- package/app/pages/product-detail/index.jsx +96 -81
- package/app/pages/product-detail/index.test.js +103 -19
- package/app/pages/product-list/index.jsx +3 -1
- package/app/pages/product-list/partials/inventory-filter.jsx +18 -21
- package/app/pages/product-list/partials/inventory-filter.test.js +15 -17
- package/app/pages/product-list/partials/selected-refinements.jsx +3 -1
- package/app/ssr.js +1 -5
- package/app/static/translations/compiled/en-GB.json +316 -30
- package/app/static/translations/compiled/en-US.json +316 -30
- package/app/static/translations/compiled/en-XA.json +673 -75
- package/app/utils/address-utils.js +112 -0
- package/app/utils/address-utils.test.js +484 -0
- package/app/utils/product-utils.js +17 -5
- package/app/utils/product-utils.test.js +17 -8
- package/app/utils/sfdc-user-agent-utils.js +32 -0
- package/app/utils/sfdc-user-agent-utils.test.js +82 -0
- package/app/utils/shipment-utils.js +196 -0
- package/app/utils/shipment-utils.test.js +458 -0
- package/app/utils/test-utils.js +4 -4
- package/app/utils/utils.js +6 -1
- package/config/default.js +4 -1
- package/config/mocks/default.js +3 -1
- package/package.json +9 -9
- package/translations/en-GB.json +127 -10
- package/translations/en-US.json +127 -10
- package/app/pages/checkout/partials/shipping-options.jsx +0 -269
|
@@ -90,7 +90,7 @@ test('ProductView Component renders properly', async () => {
|
|
|
90
90
|
expect(screen.getAllByText(/Black Single Pleat Athletic Fit Wool Suit/i)).toHaveLength(2)
|
|
91
91
|
expect(screen.getAllByText(/299\.99/)).toHaveLength(4)
|
|
92
92
|
expect(screen.getAllByText(/Add to cart/i)).toHaveLength(2)
|
|
93
|
-
expect(screen.getAllByRole('radiogroup')).toHaveLength(
|
|
93
|
+
expect(screen.getAllByRole('radiogroup')).toHaveLength(3)
|
|
94
94
|
expect(screen.getAllByText(/add to cart/i)).toHaveLength(2)
|
|
95
95
|
})
|
|
96
96
|
|
|
@@ -365,7 +365,7 @@ describe('Product Sets', () => {
|
|
|
365
365
|
name: /add set to wishlist/i
|
|
366
366
|
})[0]
|
|
367
367
|
const variationAttributes = screen
|
|
368
|
-
.
|
|
368
|
+
.queryAllByRole('radiogroup')
|
|
369
369
|
.filter(
|
|
370
370
|
(rg) =>
|
|
371
371
|
!rg.textContent.includes('Ship to Address') &&
|
|
@@ -395,7 +395,7 @@ describe('Product Sets', () => {
|
|
|
395
395
|
const addToCartButton = screen.getAllByRole('button', {name: /add to cart/i})[0]
|
|
396
396
|
const addToWishlistButton = screen.getAllByRole('button', {name: /add to wishlist/i})[0]
|
|
397
397
|
const variationAttributes = screen
|
|
398
|
-
.
|
|
398
|
+
.queryAllByRole('radiogroup')
|
|
399
399
|
.filter(
|
|
400
400
|
(rg) =>
|
|
401
401
|
!rg.textContent.includes('Ship to Address') &&
|
|
@@ -433,7 +433,7 @@ describe('Product Bundles', () => {
|
|
|
433
433
|
})[0]
|
|
434
434
|
const quantityPicker = screen.getByRole('spinbutton', {name: /quantity/i})
|
|
435
435
|
const variationAttributes = screen
|
|
436
|
-
.
|
|
436
|
+
.queryAllByRole('radiogroup')
|
|
437
437
|
.filter(
|
|
438
438
|
(rg) =>
|
|
439
439
|
!rg.textContent.includes('Ship to Address') &&
|
|
@@ -464,7 +464,7 @@ describe('Product Bundles', () => {
|
|
|
464
464
|
const addToCartButton = screen.queryByRole('button', {name: /add to cart/i})
|
|
465
465
|
const addToWishlistButton = screen.queryByRole('button', {name: /add to wishlist/i})
|
|
466
466
|
const variationAttributes = screen
|
|
467
|
-
.
|
|
467
|
+
.queryAllByRole('radiogroup')
|
|
468
468
|
.filter(
|
|
469
469
|
(rg) =>
|
|
470
470
|
!rg.textContent.includes('Ship to Address') &&
|
|
@@ -488,7 +488,7 @@ describe('Product Bundles', () => {
|
|
|
488
488
|
inventories: [{id: mockStoreData.inventoryId, orderable: true, stockLevel: 10}]
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
-
renderWithProviders(<MockComponent product={mockProduct} />)
|
|
491
|
+
renderWithProviders(<MockComponent product={mockProduct} showDeliveryOptions={true} />)
|
|
492
492
|
|
|
493
493
|
// Assert: Radio is enabled
|
|
494
494
|
const pickupRadio = await screen.findByRole('radio', {name: /pick up in store/i})
|
|
@@ -496,7 +496,9 @@ describe('Product Bundles', () => {
|
|
|
496
496
|
})
|
|
497
497
|
|
|
498
498
|
test('Pickup in store radio is disabled when inventoryId is NOT present in localStorage', async () => {
|
|
499
|
-
renderWithProviders(
|
|
499
|
+
renderWithProviders(
|
|
500
|
+
<MockComponent product={mockProductDetail} showDeliveryOptions={true} />
|
|
501
|
+
)
|
|
500
502
|
|
|
501
503
|
// Assert: Radio is disabled
|
|
502
504
|
const pickupRadio = await screen.findByRole('radio', {name: /pick up in store/i})
|
|
@@ -512,7 +514,7 @@ describe('Product Bundles', () => {
|
|
|
512
514
|
inventories: [{id: mockStoreData.inventoryId, orderable: false}]
|
|
513
515
|
}
|
|
514
516
|
|
|
515
|
-
renderWithProviders(<MockComponent product={mockProduct} />)
|
|
517
|
+
renderWithProviders(<MockComponent product={mockProduct} showDeliveryOptions={true} />)
|
|
516
518
|
|
|
517
519
|
const pickupRadio = await screen.findByRole('radio', {name: /pick up in store/i})
|
|
518
520
|
// Chakra UI does not set a semantic disabled attribute, so we test for unclickability
|
|
@@ -529,7 +531,9 @@ describe('Product Bundles', () => {
|
|
|
529
531
|
hasSelectedStore: false
|
|
530
532
|
})
|
|
531
533
|
|
|
532
|
-
renderWithProviders(
|
|
534
|
+
renderWithProviders(
|
|
535
|
+
<MockComponent product={mockProductDetail} showDeliveryOptions={true} />
|
|
536
|
+
)
|
|
533
537
|
|
|
534
538
|
const label = await screen.findByTestId('pickup-select-store-msg')
|
|
535
539
|
expect(label).toBeInTheDocument()
|
|
@@ -548,7 +552,7 @@ describe('Product Bundles', () => {
|
|
|
548
552
|
inventories: [{id: mockStoreData.inventoryId, orderable: true, stockLevel: 10}],
|
|
549
553
|
name: 'Test Product'
|
|
550
554
|
}
|
|
551
|
-
renderWithProviders(<MockComponent product={mockProduct} />)
|
|
555
|
+
renderWithProviders(<MockComponent product={mockProduct} showDeliveryOptions={true} />)
|
|
552
556
|
const msg = await screen.findByText(/In Stock at/i)
|
|
553
557
|
expect(msg).toBeInTheDocument()
|
|
554
558
|
expect(msg).toHaveTextContent(storeName)
|
|
@@ -564,7 +568,7 @@ describe('Product Bundles', () => {
|
|
|
564
568
|
inventories: [{id: mockStoreData.inventoryId, orderable: false}],
|
|
565
569
|
name: 'Test Product'
|
|
566
570
|
}
|
|
567
|
-
renderWithProviders(<MockComponent product={mockProduct} />)
|
|
571
|
+
renderWithProviders(<MockComponent product={mockProduct} showDeliveryOptions={true} />)
|
|
568
572
|
const msg = await screen.findByText(/Out of Stock at/i)
|
|
569
573
|
expect(msg).toBeInTheDocument()
|
|
570
574
|
expect(msg).toHaveTextContent(storeName)
|
|
@@ -600,13 +604,13 @@ describe('Product Bundles', () => {
|
|
|
600
604
|
expect(screen.queryByTestId('pickup-select-store-msg')).not.toBeInTheDocument()
|
|
601
605
|
})
|
|
602
606
|
|
|
603
|
-
test('
|
|
607
|
+
test('hides delivery options when showDeliveryOptions is not provided (defaults to false)', async () => {
|
|
604
608
|
renderWithProviders(<MockComponent product={mockProductDetail} />)
|
|
605
609
|
|
|
606
|
-
// Delivery options should be visible by default
|
|
607
|
-
expect(screen.
|
|
608
|
-
expect(screen.
|
|
609
|
-
expect(screen.
|
|
610
|
+
// Delivery options should not be visible by default
|
|
611
|
+
expect(screen.queryByText(/Delivery:/i)).not.toBeInTheDocument()
|
|
612
|
+
expect(screen.queryByRole('radio', {name: /ship to address/i})).not.toBeInTheDocument()
|
|
613
|
+
expect(screen.queryByRole('radio', {name: /pick up in store/i})).not.toBeInTheDocument()
|
|
610
614
|
})
|
|
611
615
|
})
|
|
612
616
|
})
|
|
@@ -619,7 +623,7 @@ test('Pick up in store radio is enabled when selected store is set', async () =>
|
|
|
619
623
|
inventories: [{id: mockStoreData.inventoryId, orderable: true, stockLevel: 10}]
|
|
620
624
|
}
|
|
621
625
|
|
|
622
|
-
renderWithProviders(<MockComponent product={mockProduct} />)
|
|
626
|
+
renderWithProviders(<MockComponent product={mockProduct} showDeliveryOptions={true} />)
|
|
623
627
|
|
|
624
628
|
// Assert: Radio is enabled
|
|
625
629
|
const pickupRadio = await screen.findByRole('radio', {name: /pick up in store/i})
|
|
@@ -627,7 +631,7 @@ test('Pick up in store radio is enabled when selected store is set', async () =>
|
|
|
627
631
|
})
|
|
628
632
|
|
|
629
633
|
test('Pick up in store radio is disabled when inventoryId is NOT present in selected store', async () => {
|
|
630
|
-
renderWithProviders(<MockComponent product={mockProductDetail} />)
|
|
634
|
+
renderWithProviders(<MockComponent product={mockProductDetail} showDeliveryOptions={true} />)
|
|
631
635
|
|
|
632
636
|
// Assert: Radio is disabled
|
|
633
637
|
const pickupRadio = await screen.findByRole('radio', {name: /pick up in store/i})
|
|
@@ -643,7 +647,7 @@ test('Pick up in store radio is disabled when inventoryId is present but product
|
|
|
643
647
|
inventories: [{id: mockStoreData.inventoryId, orderable: false}]
|
|
644
648
|
}
|
|
645
649
|
|
|
646
|
-
renderWithProviders(<MockComponent product={mockProduct} />)
|
|
650
|
+
renderWithProviders(<MockComponent product={mockProduct} showDeliveryOptions={true} />)
|
|
647
651
|
|
|
648
652
|
const pickupRadio = await screen.findByRole('radio', {name: /pick up in store/i})
|
|
649
653
|
// Chakra UI does not set a semantic disabled attribute, so we test for unclickability
|
|
@@ -660,7 +664,7 @@ test('shows "Pick up in Select Store" label when pickup is disabled due to no st
|
|
|
660
664
|
hasSelectedStore: false
|
|
661
665
|
})
|
|
662
666
|
|
|
663
|
-
renderWithProviders(<MockComponent product={mockProductDetail} />)
|
|
667
|
+
renderWithProviders(<MockComponent product={mockProductDetail} showDeliveryOptions={true} />)
|
|
664
668
|
|
|
665
669
|
const label = await screen.findByTestId('pickup-select-store-msg')
|
|
666
670
|
expect(label).toBeInTheDocument()
|
|
@@ -676,7 +680,7 @@ test('shows "In stock at {storeName}" when store has inventory', async () => {
|
|
|
676
680
|
inventories: [{id: mockStoreData.inventoryId, orderable: true, stockLevel: 10}]
|
|
677
681
|
}
|
|
678
682
|
|
|
679
|
-
renderWithProviders(<MockComponent product={mockProduct} />)
|
|
683
|
+
renderWithProviders(<MockComponent product={mockProduct} showDeliveryOptions={true} />)
|
|
680
684
|
const msg = await screen.findByText(/In stock at/i)
|
|
681
685
|
expect(msg).toBeInTheDocument()
|
|
682
686
|
expect(msg).toHaveTextContent('Test Store')
|
|
@@ -21,36 +21,35 @@ describe('QuantityPicker', () => {
|
|
|
21
21
|
test('clicking plus increments value', async () => {
|
|
22
22
|
const user = userEvent.setup()
|
|
23
23
|
renderWithProviders(<MockComponent />)
|
|
24
|
-
const input = screen.getByRole('spinbutton')
|
|
25
24
|
const button = screen.getByText('+')
|
|
26
25
|
await user.click(button)
|
|
26
|
+
const input = screen.getByRole('spinbutton')
|
|
27
27
|
expect(input.value).toBe('6')
|
|
28
28
|
})
|
|
29
29
|
test('clicking minus decrements value', async () => {
|
|
30
30
|
const user = userEvent.setup()
|
|
31
31
|
renderWithProviders(<MockComponent />)
|
|
32
|
-
const input = screen.getByRole('spinbutton')
|
|
33
32
|
const button = screen.getByText(MINUS)
|
|
34
33
|
await user.click(button)
|
|
34
|
+
const input = screen.getByRole('spinbutton')
|
|
35
35
|
expect(input.value).toBe('4')
|
|
36
36
|
})
|
|
37
37
|
test('typing enter/space on plus increments value', async () => {
|
|
38
38
|
const user = userEvent.setup()
|
|
39
39
|
renderWithProviders(<MockComponent />)
|
|
40
|
-
const input = screen.getByRole('spinbutton')
|
|
41
40
|
const button = screen.getByText('+')
|
|
42
41
|
await user.type(button, '{enter}')
|
|
42
|
+
const input = screen.getByRole('spinbutton')
|
|
43
43
|
expect(input.value).toBe('6')
|
|
44
44
|
await user.type(button, '{space}')
|
|
45
45
|
expect(input.value).toBe('7')
|
|
46
46
|
})
|
|
47
47
|
|
|
48
48
|
test('keydown enter/space on plus increments value', async () => {
|
|
49
|
-
const user = userEvent.setup()
|
|
50
49
|
renderWithProviders(<MockComponent />)
|
|
51
|
-
const input = screen.getByRole('spinbutton')
|
|
52
50
|
const button = screen.getByText('+')
|
|
53
51
|
fireEvent.keyDown(button, {key: 'Enter'})
|
|
52
|
+
const input = screen.getByRole('spinbutton')
|
|
54
53
|
expect(input.value).toBe('6')
|
|
55
54
|
fireEvent.keyDown(button, {key: ' '})
|
|
56
55
|
expect(input.value).toBe('7')
|
|
@@ -59,20 +58,19 @@ describe('QuantityPicker', () => {
|
|
|
59
58
|
test('typing space on minus decrements value', async () => {
|
|
60
59
|
const user = userEvent.setup()
|
|
61
60
|
renderWithProviders(<MockComponent />)
|
|
62
|
-
const input = screen.getByRole('spinbutton')
|
|
63
61
|
const button = screen.getByText(MINUS)
|
|
64
62
|
await user.type(button, '{enter}')
|
|
63
|
+
const input = screen.getByRole('spinbutton')
|
|
65
64
|
expect(input.value).toBe('4')
|
|
66
65
|
await user.type(button, '{space}')
|
|
67
66
|
expect(input.value).toBe('3')
|
|
68
67
|
})
|
|
69
68
|
|
|
70
69
|
test('keydown enter/space on minus decrements value', async () => {
|
|
71
|
-
const user = userEvent.setup()
|
|
72
70
|
renderWithProviders(<MockComponent />)
|
|
73
|
-
const input = screen.getByRole('spinbutton')
|
|
74
71
|
const button = screen.getByText(MINUS)
|
|
75
72
|
fireEvent.keyDown(button, {key: 'Enter'})
|
|
73
|
+
const input = screen.getByRole('spinbutton')
|
|
76
74
|
expect(input.value).toBe('4')
|
|
77
75
|
fireEvent.keyDown(button, {key: ' '})
|
|
78
76
|
expect(input.value).toBe('3')
|
|
@@ -81,16 +79,18 @@ describe('QuantityPicker', () => {
|
|
|
81
79
|
test('plus button is tabbable', async () => {
|
|
82
80
|
const user = userEvent.setup()
|
|
83
81
|
renderWithProviders(<MockComponent />)
|
|
84
|
-
|
|
85
|
-
await user.
|
|
82
|
+
await user.tab()
|
|
83
|
+
await user.tab() // Tab twice to get to the plus button
|
|
84
|
+
await user.tab() // Tab once more to get to the plus button
|
|
86
85
|
const button = screen.getByText('+')
|
|
87
86
|
expect(button).toHaveFocus()
|
|
88
87
|
})
|
|
89
88
|
test('minus button is tabbable', async () => {
|
|
90
89
|
const user = userEvent.setup()
|
|
91
90
|
renderWithProviders(<MockComponent />)
|
|
92
|
-
|
|
93
|
-
await user.
|
|
91
|
+
await user.tab({shift: true})
|
|
92
|
+
await user.tab({shift: true}) // Shift+tab twice to get to the minus button
|
|
93
|
+
await user.tab({shift: true}) // Shift+tab once more to get to the minus button
|
|
94
94
|
const button = screen.getByText(MINUS)
|
|
95
95
|
expect(button).toHaveFocus()
|
|
96
96
|
})
|
|
@@ -49,7 +49,7 @@ const MockedErrorComponent = () => {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
test('Allows customer to generate password token and see success message', async () => {
|
|
52
|
-
const mockSubmitForm = jest.fn(async (
|
|
52
|
+
const mockSubmitForm = jest.fn(async () => ({
|
|
53
53
|
password: jest.fn(async (passwordData) => {
|
|
54
54
|
// Mock behavior inside the password function
|
|
55
55
|
console.log('Password function called with:', passwordData)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023, 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
|
+
|
|
8
|
+
export {AlertDescription} from '@chakra-ui/react'
|
|
@@ -19,6 +19,7 @@ export {AlertDialogHeader} from './AlertDialogHeader'
|
|
|
19
19
|
export {AlertDialogOverlay} from './AlertDialogOverlay'
|
|
20
20
|
export {AlertIcon} from './AlertIcon'
|
|
21
21
|
export {AlertTitle} from './AlertTitle'
|
|
22
|
+
export {AlertDescription} from './AlertDescription'
|
|
22
23
|
export {AspectRatio} from './AspectRatio'
|
|
23
24
|
export {Badge} from './Badge'
|
|
24
25
|
export {Box} from './Box'
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import React from 'react'
|
|
8
8
|
import PropTypes from 'prop-types'
|
|
9
|
-
import {useIntl} from 'react-intl'
|
|
9
|
+
import {useIntl, FormattedMessage} from 'react-intl'
|
|
10
10
|
import {
|
|
11
11
|
Box,
|
|
12
|
+
Button,
|
|
13
|
+
Flex,
|
|
12
14
|
Accordion,
|
|
13
15
|
AccordionItem,
|
|
14
16
|
AccordionButton,
|
|
@@ -25,7 +27,8 @@ const StoreDisplay = ({
|
|
|
25
27
|
nameStyle = {fontSize: 'md', fontWeight: 'bold'},
|
|
26
28
|
textSize = 'sm',
|
|
27
29
|
accordionButtonStyle = {},
|
|
28
|
-
accordionPanelStyle = {}
|
|
30
|
+
accordionPanelStyle = {},
|
|
31
|
+
onChangeStore
|
|
29
32
|
}) => {
|
|
30
33
|
const intl = useIntl()
|
|
31
34
|
|
|
@@ -35,7 +38,26 @@ const StoreDisplay = ({
|
|
|
35
38
|
|
|
36
39
|
return (
|
|
37
40
|
<Box id={`store-info-${store.id}`}>
|
|
38
|
-
{store.name &&
|
|
41
|
+
{store.name && (
|
|
42
|
+
<Flex justify="space-between" align="flex-start" mb={1}>
|
|
43
|
+
<Box {...nameStyle}>{store.name}</Box>
|
|
44
|
+
{onChangeStore && (
|
|
45
|
+
<Button
|
|
46
|
+
variant="link"
|
|
47
|
+
size="sm"
|
|
48
|
+
fontWeight="normal"
|
|
49
|
+
onClick={onChangeStore}
|
|
50
|
+
data-testid="change-store-button"
|
|
51
|
+
ml={2}
|
|
52
|
+
>
|
|
53
|
+
<FormattedMessage
|
|
54
|
+
defaultMessage="Use Recent Store"
|
|
55
|
+
id="store_display.button.use_recent_store"
|
|
56
|
+
/>
|
|
57
|
+
</Button>
|
|
58
|
+
)}
|
|
59
|
+
</Flex>
|
|
60
|
+
)}
|
|
39
61
|
<Box fontSize={textSize} color="gray.600">
|
|
40
62
|
{store.address1}
|
|
41
63
|
</Box>
|
|
@@ -161,7 +183,9 @@ StoreDisplay.propTypes = {
|
|
|
161
183
|
/** Custom style props for accordion button */
|
|
162
184
|
accordionButtonStyle: PropTypes.object,
|
|
163
185
|
/** Custom style props for accordion panel */
|
|
164
|
-
accordionPanelStyle: PropTypes.object
|
|
186
|
+
accordionPanelStyle: PropTypes.object,
|
|
187
|
+
/** Callback function to handle change store action */
|
|
188
|
+
onChangeStore: PropTypes.func
|
|
165
189
|
}
|
|
166
190
|
|
|
167
191
|
export default StoreDisplay
|
|
@@ -266,4 +266,75 @@ describe('StoreDisplay component', () => {
|
|
|
266
266
|
expect(screen.queryByText(/Phone:/)).not.toBeInTheDocument()
|
|
267
267
|
expect(screen.queryByText(/Email:/)).not.toBeInTheDocument()
|
|
268
268
|
})
|
|
269
|
+
|
|
270
|
+
describe('Change Store Button', () => {
|
|
271
|
+
test('renders Change Store button when onChangeStore is provided', () => {
|
|
272
|
+
const mockOnChangeStore = jest.fn()
|
|
273
|
+
renderWithProviders(
|
|
274
|
+
<StoreDisplay store={mockStore} onChangeStore={mockOnChangeStore} />
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
const changeStoreButton = screen.getByTestId('change-store-button')
|
|
278
|
+
expect(changeStoreButton).toBeInTheDocument()
|
|
279
|
+
expect(changeStoreButton).toHaveTextContent('Use Recent Store')
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test('does not render Change Store button when onChangeStore is not provided', () => {
|
|
283
|
+
renderWithProviders(<StoreDisplay store={mockStore} />)
|
|
284
|
+
|
|
285
|
+
expect(screen.queryByTestId('change-store-button')).not.toBeInTheDocument()
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('calls onChangeStore when Change Store button is clicked', async () => {
|
|
289
|
+
const mockOnChangeStore = jest.fn()
|
|
290
|
+
const user = userEvent.setup()
|
|
291
|
+
renderWithProviders(
|
|
292
|
+
<StoreDisplay store={mockStore} onChangeStore={mockOnChangeStore} />
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
const changeStoreButton = screen.getByTestId('change-store-button')
|
|
296
|
+
await user.click(changeStoreButton)
|
|
297
|
+
|
|
298
|
+
expect(mockOnChangeStore).toHaveBeenCalledTimes(1)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test('does not render Change Store button when store has no name', () => {
|
|
302
|
+
const storeWithoutName = {
|
|
303
|
+
...mockStore,
|
|
304
|
+
name: null
|
|
305
|
+
}
|
|
306
|
+
const mockOnChangeStore = jest.fn()
|
|
307
|
+
renderWithProviders(
|
|
308
|
+
<StoreDisplay store={storeWithoutName} onChangeStore={mockOnChangeStore} />
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
expect(screen.queryByTestId('change-store-button')).not.toBeInTheDocument()
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('renders Change Store button with correct styling', () => {
|
|
315
|
+
const mockOnChangeStore = jest.fn()
|
|
316
|
+
renderWithProviders(
|
|
317
|
+
<StoreDisplay store={mockStore} onChangeStore={mockOnChangeStore} />
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
const changeStoreButton = screen.getByTestId('change-store-button')
|
|
321
|
+
expect(changeStoreButton).toBeInTheDocument()
|
|
322
|
+
|
|
323
|
+
// Check that it's rendered as a button
|
|
324
|
+
expect(changeStoreButton.tagName).toBe('BUTTON')
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test('positions Change Store button next to store name', () => {
|
|
328
|
+
const mockOnChangeStore = jest.fn()
|
|
329
|
+
renderWithProviders(
|
|
330
|
+
<StoreDisplay store={mockStore} onChangeStore={mockOnChangeStore} />
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
const storeName = screen.getByText('Downtown Store')
|
|
334
|
+
const changeStoreButton = screen.getByTestId('change-store-button')
|
|
335
|
+
|
|
336
|
+
// Both should be in the same parent container (Flex)
|
|
337
|
+
expect(storeName.parentElement).toBe(changeStoreButton.parentElement)
|
|
338
|
+
})
|
|
339
|
+
})
|
|
269
340
|
})
|
|
@@ -9,6 +9,7 @@ import React from 'react'
|
|
|
9
9
|
import {render, screen, waitFor} from '@testing-library/react'
|
|
10
10
|
import userEvent from '@testing-library/user-event'
|
|
11
11
|
import {IntlProvider} from 'react-intl'
|
|
12
|
+
import {MemoryRouter} from 'react-router-dom'
|
|
12
13
|
import {StoreLocatorForm} from '@salesforce/retail-react-app/app/components/store-locator/form'
|
|
13
14
|
import {useStoreLocator} from '@salesforce/retail-react-app/app/hooks/use-store-locator'
|
|
14
15
|
import {useGeolocation} from '@salesforce/retail-react-app/app/hooks/use-geo-location'
|
|
@@ -75,7 +76,9 @@ describe('StoreLocatorForm', () => {
|
|
|
75
76
|
const TestWrapper = ({children}) => (
|
|
76
77
|
<IntlProvider locale="en" messages={messages}>
|
|
77
78
|
<MultiSiteProvider site={mockSite}>
|
|
78
|
-
<
|
|
79
|
+
<MemoryRouter>
|
|
80
|
+
<StoreLocatorProvider config={mockConfig}>{children}</StoreLocatorProvider>
|
|
81
|
+
</MemoryRouter>
|
|
79
82
|
</MultiSiteProvider>
|
|
80
83
|
</IntlProvider>
|
|
81
84
|
)
|
|
@@ -92,7 +95,10 @@ describe('StoreLocatorForm', () => {
|
|
|
92
95
|
config: mockConfig,
|
|
93
96
|
formValues: {countryCode: '', postalCode: ''},
|
|
94
97
|
setFormValues: mockSetFormValues,
|
|
95
|
-
setDeviceCoordinates: mockSetDeviceCoordinates
|
|
98
|
+
setDeviceCoordinates: mockSetDeviceCoordinates,
|
|
99
|
+
isOpen: false,
|
|
100
|
+
onOpen: jest.fn(),
|
|
101
|
+
onClose: jest.fn()
|
|
96
102
|
}))
|
|
97
103
|
|
|
98
104
|
useGeolocation.mockImplementation(() => ({
|
|
@@ -197,7 +203,10 @@ describe('StoreLocatorForm', () => {
|
|
|
197
203
|
config: {...mockConfig, supportedCountries: []},
|
|
198
204
|
formValues: {countryCode: '', postalCode: ''},
|
|
199
205
|
setFormValues: mockSetFormValues,
|
|
200
|
-
setDeviceCoordinates: mockSetDeviceCoordinates
|
|
206
|
+
setDeviceCoordinates: mockSetDeviceCoordinates,
|
|
207
|
+
isOpen: false,
|
|
208
|
+
onOpen: jest.fn(),
|
|
209
|
+
onClose: jest.fn()
|
|
201
210
|
}))
|
|
202
211
|
|
|
203
212
|
renderWithProviders(<StoreLocatorForm />)
|
|
@@ -220,7 +229,10 @@ describe('StoreLocatorForm', () => {
|
|
|
220
229
|
config: mockConfig,
|
|
221
230
|
formValues: {countryCode: '', postalCode: ''},
|
|
222
231
|
setFormValues: mockSetFormValues,
|
|
223
|
-
setDeviceCoordinates: mockSetDeviceCoordinates
|
|
232
|
+
setDeviceCoordinates: mockSetDeviceCoordinates,
|
|
233
|
+
isOpen: false,
|
|
234
|
+
onOpen: jest.fn(),
|
|
235
|
+
onClose: jest.fn()
|
|
224
236
|
}))
|
|
225
237
|
|
|
226
238
|
renderWithProviders(<StoreLocatorForm />)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright (c)
|
|
2
|
+
* Copyright (c) 2025, salesforce.com, inc.
|
|
3
3
|
* All rights reserved.
|
|
4
4
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
5
5
|
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
@@ -12,6 +12,7 @@ import {StoreLocatorListItem} from '@salesforce/retail-react-app/app/components/
|
|
|
12
12
|
import {useStoreLocator} from '@salesforce/retail-react-app/app/hooks/use-store-locator'
|
|
13
13
|
import {useSelectedStore} from '@salesforce/retail-react-app/app/hooks/use-selected-store'
|
|
14
14
|
import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
|
|
15
|
+
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
|
|
15
16
|
|
|
16
17
|
export const StoreLocatorList = () => {
|
|
17
18
|
const intl = useIntl()
|
|
@@ -22,7 +23,11 @@ export const StoreLocatorList = () => {
|
|
|
22
23
|
const [page, setPage] = useState(1)
|
|
23
24
|
const [initialSelectedStoreId, setInitialSelectedStoreId] = useState(selectedStoreId)
|
|
24
25
|
|
|
26
|
+
// with BOPIS enabled: store locator selector can't be changed unless basket is emty
|
|
27
|
+
// with BOPIS and MULTI_SHIP enabled, store locator can be changed any time
|
|
25
28
|
const hasItemsInBasket = derivedData?.totalItems > 0
|
|
29
|
+
const multishipEnabled = getConfig()?.app?.multishipEnabled ?? true
|
|
30
|
+
const storeSelectionDisabled = multishipEnabled ? false : hasItemsInBasket
|
|
26
31
|
|
|
27
32
|
useEffect(() => {
|
|
28
33
|
setPage(1)
|
|
@@ -31,7 +36,7 @@ export const StoreLocatorList = () => {
|
|
|
31
36
|
}, [data])
|
|
32
37
|
|
|
33
38
|
const handleChange = (selectedStoreId) => {
|
|
34
|
-
if (!
|
|
39
|
+
if (!storeSelectionDisabled) {
|
|
35
40
|
setSelectedStoreId(selectedStoreId)
|
|
36
41
|
}
|
|
37
42
|
}
|
|
@@ -47,7 +52,7 @@ export const StoreLocatorList = () => {
|
|
|
47
52
|
id: 'store_locator.description.no_locations',
|
|
48
53
|
defaultMessage: 'Sorry, there are no locations in this area.'
|
|
49
54
|
})
|
|
50
|
-
if (hasItemsInBasket) {
|
|
55
|
+
if (storeSelectionDisabled && hasItemsInBasket) {
|
|
51
56
|
return intl.formatMessage({
|
|
52
57
|
id: 'store_locator.error.items_in_basket',
|
|
53
58
|
defaultMessage: 'To change your selected store, remove all items from your cart.'
|
|
@@ -145,7 +150,7 @@ export const StoreLocatorList = () => {
|
|
|
145
150
|
value: store.id,
|
|
146
151
|
isChecked: selectedStoreId === store.id,
|
|
147
152
|
'aria-describedby': `store-info-${store.id}`,
|
|
148
|
-
isDisabled: !store.inventoryId ||
|
|
153
|
+
isDisabled: !store.inventoryId || storeSelectionDisabled
|
|
149
154
|
}}
|
|
150
155
|
/>
|
|
151
156
|
))}
|
|
@@ -31,8 +31,10 @@ export const ToggleCard = ({
|
|
|
31
31
|
disableEdit,
|
|
32
32
|
onEdit,
|
|
33
33
|
editLabel,
|
|
34
|
+
editAction,
|
|
34
35
|
isLoading,
|
|
35
36
|
children,
|
|
37
|
+
onEditActionClick,
|
|
36
38
|
...props
|
|
37
39
|
}) => {
|
|
38
40
|
const titleRef = useRef()
|
|
@@ -79,6 +81,16 @@ export const ToggleCard = ({
|
|
|
79
81
|
)}
|
|
80
82
|
</Button>
|
|
81
83
|
)}
|
|
84
|
+
{editing && editAction && onEditActionClick && (
|
|
85
|
+
<Button
|
|
86
|
+
variant="link"
|
|
87
|
+
size="sm"
|
|
88
|
+
onClick={onEditActionClick}
|
|
89
|
+
aria-label={editAction}
|
|
90
|
+
>
|
|
91
|
+
{editAction}
|
|
92
|
+
</Button>
|
|
93
|
+
)}
|
|
82
94
|
</Flex>
|
|
83
95
|
<Box data-testid={`sf-toggle-card-${id}-content`}>{children}</Box>
|
|
84
96
|
</Stack>
|
|
@@ -108,6 +120,8 @@ ToggleCard.propTypes = {
|
|
|
108
120
|
disabled: PropTypes.bool,
|
|
109
121
|
disableEdit: PropTypes.bool,
|
|
110
122
|
onEdit: PropTypes.func,
|
|
123
|
+
editAction: PropTypes.string,
|
|
124
|
+
onEditActionClick: PropTypes.func,
|
|
111
125
|
children: PropTypes.any
|
|
112
126
|
}
|
|
113
127
|
|
|
@@ -31,14 +31,24 @@ const UnavailableProductConfirmationModal = ({
|
|
|
31
31
|
}) => {
|
|
32
32
|
const unavailableProductIdsRef = useRef(null)
|
|
33
33
|
const ids = productIds.length ? productIds : productItems.map((i) => i.productId)
|
|
34
|
+
const uniqueInventoryIds = [
|
|
35
|
+
...new Set(productItems.map((i) => i.inventoryId).filter(Boolean))
|
|
36
|
+
].join(',')
|
|
37
|
+
|
|
34
38
|
useProducts(
|
|
35
|
-
{
|
|
39
|
+
{
|
|
40
|
+
parameters: {
|
|
41
|
+
ids: ids?.join(','),
|
|
42
|
+
allImages: true,
|
|
43
|
+
...(uniqueInventoryIds ? {inventoryIds: uniqueInventoryIds} : {})
|
|
44
|
+
}
|
|
45
|
+
},
|
|
36
46
|
{
|
|
37
47
|
enabled: ids?.length > 0,
|
|
38
48
|
onSuccess: (result) => {
|
|
39
49
|
const resProductIds = []
|
|
40
50
|
const unOrderableIds = []
|
|
41
|
-
result.data?.forEach(({id, inventory}) => {
|
|
51
|
+
result.data?.forEach(({id, inventory, inventories}) => {
|
|
42
52
|
// when a product is unavailable, the getProducts will not return its product detail.
|
|
43
53
|
// we compare the response ids with the ones in basket to figure which product has become unavailable
|
|
44
54
|
resProductIds.push(id)
|
|
@@ -50,11 +60,15 @@ const UnavailableProductConfirmationModal = ({
|
|
|
50
60
|
const productItem = productItems.find((item) => item.productId === id)
|
|
51
61
|
// wishlist item will have the property type
|
|
52
62
|
const isWishlist = !!productItem?.type
|
|
63
|
+
// inventory for the product's pickup store or the delivery inventory
|
|
64
|
+
const productItemInventory =
|
|
65
|
+
inventories?.find((entry) => entry.id === productItem.inventoryId) ||
|
|
66
|
+
inventory
|
|
53
67
|
if (
|
|
54
68
|
!isWishlist &&
|
|
55
|
-
(!
|
|
56
|
-
(
|
|
57
|
-
productItem?.quantity >
|
|
69
|
+
(!productItemInventory?.orderable ||
|
|
70
|
+
(productItemInventory?.orderable &&
|
|
71
|
+
productItem?.quantity > productItemInventory.stockLevel))
|
|
58
72
|
) {
|
|
59
73
|
unOrderableIds.push(id)
|
|
60
74
|
}
|