@salesforce/retail-react-app 7.0.0-preview.0 → 7.0.0

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 (38) hide show
  1. package/CHANGELOG.md +9 -8
  2. package/app/components/dynamic-image/index.jsx +91 -16
  3. package/app/components/dynamic-image/index.test.js +214 -30
  4. package/app/components/image/index.jsx +5 -13
  5. package/app/components/image/index.test.js +6 -3
  6. package/app/components/island/README.md +15 -10
  7. package/app/components/island/index.jsx +12 -5
  8. package/app/components/island/index.test.js +35 -0
  9. package/app/components/passwordless-login/index.jsx +4 -5
  10. package/app/components/passwordless-login/index.test.js +2 -4
  11. package/app/components/product-tile/index.jsx +1 -1
  12. package/app/components/product-view-modal/bundle.jsx +12 -2
  13. package/app/components/social-login/index.jsx +1 -0
  14. package/app/components/standard-login/index.jsx +4 -1
  15. package/app/constants.js +3 -0
  16. package/app/hooks/use-auth-modal.js +68 -67
  17. package/app/hooks/use-auth-modal.test.js +93 -23
  18. package/app/hooks/use-datacloud.js +169 -192
  19. package/app/hooks/use-datacloud.test.js +273 -17
  20. package/app/pages/cart/index.jsx +2 -1
  21. package/app/pages/cart/partials/cart-secondary-button-group.jsx +8 -10
  22. package/app/pages/cart/partials/cart-secondary-button-group.test.js +2 -3
  23. package/app/pages/checkout/partials/contact-info.jsx +9 -8
  24. package/app/pages/checkout/partials/contact-info.test.js +41 -4
  25. package/app/pages/checkout/partials/login-state.jsx +3 -3
  26. package/app/pages/home/index.test.js +2 -1
  27. package/app/pages/login/index.jsx +37 -37
  28. package/app/pages/login/index.test.js +42 -0
  29. package/app/pages/product-detail/index.jsx +64 -73
  30. package/app/pages/product-list/index.jsx +19 -9
  31. package/app/pages/product-list/index.test.js +153 -19
  32. package/app/utils/image.js +29 -0
  33. package/app/utils/image.test.js +141 -1
  34. package/app/utils/responsive-image.js +197 -115
  35. package/app/utils/responsive-image.test.js +483 -133
  36. package/config/default.js +2 -2
  37. package/config/mocks/default.js +2 -2
  38. package/package.json +7 -7
@@ -32,13 +32,12 @@ import {
32
32
  INVALID_TOKEN_ERROR,
33
33
  INVALID_TOKEN_ERROR_MESSAGE,
34
34
  FEATURE_UNAVAILABLE_ERROR_MESSAGE,
35
- LOGIN_TYPES,
36
35
  PASSWORDLESS_LOGIN_LANDING_PATH,
37
36
  PASSWORDLESS_ERROR_MESSAGES,
38
37
  USER_NOT_FOUND_ERROR
39
38
  } from '@salesforce/retail-react-app/app/constants'
40
39
  import {usePrevious} from '@salesforce/retail-react-app/app/hooks/use-previous'
41
- import {isServer} from '@salesforce/retail-react-app/app/utils/utils'
40
+ import {isServer, noop} from '@salesforce/retail-react-app/app/utils/utils'
42
41
  import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
43
42
 
44
43
  const LOGIN_ERROR_MESSAGE = defineMessage({
@@ -76,7 +75,6 @@ const Login = ({initialView = LOGIN_VIEW}) => {
76
75
  const mergeBasket = useShopperBasketsMutation('mergeBasket')
77
76
  const [currentView, setCurrentView] = useState(initialView)
78
77
  const [passwordlessLoginEmail, setPasswordlessLoginEmail] = useState('')
79
- const [loginType, setLoginType] = useState(LOGIN_TYPES.PASSWORD)
80
78
  const [redirectPath, setRedirectPath] = useState('')
81
79
 
82
80
  const handleMergeBasket = () => {
@@ -107,39 +105,41 @@ const Login = ({initialView = LOGIN_VIEW}) => {
107
105
  }
108
106
  }
109
107
 
110
- const submitForm = async (data) => {
111
- form.clearErrors()
112
-
113
- const handlePasswordlessLogin = async (email) => {
114
- try {
115
- await authorizePasswordlessLogin.mutateAsync({userid: email})
116
- setCurrentView(EMAIL_VIEW)
117
- } catch (error) {
118
- const message = USER_NOT_FOUND_ERROR.test(error.message)
119
- ? formatMessage(CREATE_ACCOUNT_FIRST_ERROR_MESSAGE)
120
- : PASSWORDLESS_ERROR_MESSAGES.some((msg) => msg.test(error.message))
121
- ? formatMessage(FEATURE_UNAVAILABLE_ERROR_MESSAGE)
122
- : formatMessage(API_ERROR_MESSAGE)
123
- form.setError('global', {type: 'manual', message})
124
- }
108
+ const handlePasswordlessLogin = async (email) => {
109
+ try {
110
+ await authorizePasswordlessLogin.mutateAsync({userid: email})
111
+ setPasswordlessLoginEmail(email)
112
+ setCurrentView(EMAIL_VIEW)
113
+ } catch (error) {
114
+ const message = USER_NOT_FOUND_ERROR.test(error.message)
115
+ ? formatMessage(CREATE_ACCOUNT_FIRST_ERROR_MESSAGE)
116
+ : PASSWORDLESS_ERROR_MESSAGES.some((msg) => msg.test(error.message))
117
+ ? formatMessage(FEATURE_UNAVAILABLE_ERROR_MESSAGE)
118
+ : formatMessage(API_ERROR_MESSAGE)
119
+ form.setError('global', {type: 'manual', message})
125
120
  }
121
+ }
122
+
123
+ const submitForm = async (data, isPasswordless = false) => {
124
+ form.clearErrors()
126
125
 
127
126
  return {
128
127
  login: async (data) => {
129
- if (loginType === LOGIN_TYPES.PASSWORD) {
130
- try {
131
- await login.mutateAsync({username: data.email, password: data.password})
132
- } catch (error) {
133
- const message = /Unauthorized/i.test(error.message)
134
- ? formatMessage(LOGIN_ERROR_MESSAGE)
135
- : formatMessage(API_ERROR_MESSAGE)
136
- form.setError('global', {type: 'manual', message})
137
- }
138
- handleMergeBasket()
139
- } else if (loginType === LOGIN_TYPES.PASSWORDLESS) {
140
- setPasswordlessLoginEmail(data.email)
141
- await handlePasswordlessLogin(data.email)
128
+ if (isPasswordless) {
129
+ const email = data.email
130
+ await handlePasswordlessLogin(email)
131
+ return
142
132
  }
133
+
134
+ try {
135
+ await login.mutateAsync({username: data.email, password: data.password})
136
+ } catch (error) {
137
+ const message = /Unauthorized/i.test(error.message)
138
+ ? formatMessage(LOGIN_ERROR_MESSAGE)
139
+ : formatMessage(API_ERROR_MESSAGE)
140
+ form.setError('global', {type: 'manual', message})
141
+ }
142
+ handleMergeBasket()
143
143
  },
144
144
  email: async () => {
145
145
  await handlePasswordlessLogin(passwordlessLoginEmail)
@@ -204,19 +204,19 @@ const Login = ({initialView = LOGIN_VIEW}) => {
204
204
  {!form.formState.isSubmitSuccessful && currentView === LOGIN_VIEW && (
205
205
  <LoginForm
206
206
  form={form}
207
- submitForm={submitForm}
208
- clickCreateAccount={() => navigate('/registration')}
209
- handlePasswordlessLoginClick={() => {
210
- setLoginType(LOGIN_TYPES.PASSWORDLESS)
207
+ submitForm={(data) => {
208
+ const shouldUsePasswordless = isPasswordlessEnabled && !data.password
209
+ return submitForm(data, shouldUsePasswordless)
211
210
  }}
211
+ clickCreateAccount={() => navigate('/registration')}
212
+ handlePasswordlessLoginClick={noop}
212
213
  handleForgotPasswordClick={() => navigate('/reset-password')}
213
214
  isPasswordlessEnabled={isPasswordlessEnabled}
214
215
  isSocialEnabled={isSocialEnabled}
215
216
  idps={idps}
216
- setLoginType={setLoginType}
217
217
  />
218
218
  )}
219
- {form.formState.isSubmitSuccessful && currentView === EMAIL_VIEW && (
219
+ {currentView === EMAIL_VIEW && (
220
220
  <PasswordlessEmailConfirmation
221
221
  form={form}
222
222
  submitForm={submitForm}
@@ -33,6 +33,7 @@ const MockedComponent = () => {
33
33
  const match = {
34
34
  params: {pageName: 'profile'}
35
35
  }
36
+
36
37
  return (
37
38
  <Router>
38
39
  <Login />
@@ -169,6 +170,47 @@ describe('Logging in tests', function () {
169
170
  expect(screen.getByText(/My Profile/i)).toBeInTheDocument()
170
171
  })
171
172
  })
173
+
174
+ test('allows customer to sign in via Enter key', async () => {
175
+ const {user} = renderWithProviders(<MockedComponent />, {
176
+ wrapperProps: {
177
+ siteAlias: 'uk',
178
+ locale: {id: 'en-GB'},
179
+ appConfig: mockConfig.app,
180
+ bypassAuth: false
181
+ }
182
+ })
183
+
184
+ // enter credentials
185
+ await user.type(screen.getByLabelText('Email'), 'customer@test.com')
186
+ await user.type(screen.getByLabelText('Password'), 'Password!1')
187
+
188
+ // mock successful login response
189
+ global.server.use(
190
+ rest.post('*/oauth2/token', (req, res, ctx) =>
191
+ res(
192
+ ctx.delay(0),
193
+ ctx.json({
194
+ customer_id: 'customerid_1',
195
+ access_token:
196
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXQiOiJHVUlEIiwic2NwIjoic2ZjYy5zaG9wcGVyLW15YWNjb3VudC5iYXNrZXRzIHNmY2Muc2hvcHBlci1teWFjY291bnQuYWRkcmVzc2VzIHNmY2Muc2hvcHBlci1wcm9kdWN0cyBzZmNjLnNob3BwZXItZGlzY292ZXJ5LXNlYXJjaCBzZmNjLnNob3BwZXItbXlhY2NvdW50LnJ3IHNmY2Muc2hvcHBlci1teWFjY291bnQucGF5bWVudGluc3RydW1lbnRzIHNmY2Muc2hvcHBlci1jdXN0b21lcnMubG9naW4gc2ZjYy5zaG9wcGVyLWV4cGVyaWVuY2Ugc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5vcmRlcnMgc2ZjYy5zaG9wcGVyLWN1c3RvbWVycy5yZWdpc3RlciBzZmNjLnNob3BwZXItYmFza2V0cy1vcmRlcnMgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5hZGRyZXNzZXMucncgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5wcm9kdWN0bGlzdHMucncgc2ZjYy5zaG9wcGVyLXByb2R1Y3RsaXN0cyBzZmNjLnNob3BwZXItcHJvbW90aW9ucyBzZmNjLnNob3BwZXItYmFza2V0cy1vcmRlcnMucncgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5wYXltZW50aW5zdHJ1bWVudHMucncgc2ZjYy5zaG9wcGVyLWdpZnQtY2VydGlmaWNhdGVzIHNmY2Muc2hvcHBlci1wcm9kdWN0LXNlYXJjaCBzZmNjLnNob3BwZXItbXlhY2NvdW50LnByb2R1Y3RsaXN0cyBzZmNjLnNob3BwZXItY2F0ZWdvcmllcyBzZmNjLnNob3BwZXItbXlhY2NvdW50Iiwic3ViIjoiY2Mtc2xhczo6enpyZl8wMDE6OnNjaWQ6YzljNDViZmQtMGVkMy00YWEyLTk5NzEtNDBmODg5NjJiODM2Ojp1c2lkOjhlODgzOTczLTY4ZWItNDFmZS1hM2M1LTc1NjIzMjY1MmZmNSIsImN0eCI6InNsYXMiLCJpc3MiOiJzbGFzL3Byb2QvenpyZl8wMDEiLCJpc3QiOjEsImF1ZCI6ImNvbW1lcmNlY2xvdWQvcHJvZC96enJmXzAwMSIsIm5iZiI6MTY3ODgzNDI3MSwic3R5IjoiVXNlciIsImlzYiI6InVpZG86ZWNvbTo6dXBuOmtldjVAdGVzdC5jb206OnVpZG46a2V2aW4gaGU6OmdjaWQ6YWJtZXMybWJrM2xYa1JsSEZKd0dZWWt1eEo6OnJjaWQ6YWJVTXNhdnBEOVk2alcwMGRpMlNqeEdDTVU6OmNoaWQ6UmVmQXJjaEdsb2JhbCIsImV4cCI6MjY3ODgzNjEwMSwiaWF0IjoxNjc4ODM0MzAxLCJqdGkiOiJDMkM0ODU2MjAxODYwLTE4OTA2Nzg5MDM0ODA1ODMyNTcwNjY2NTQyIn0._tUrxeXdFYPj6ZoY-GILFRd3-aD1RGPkZX6TqHeS494',
197
+ refresh_token: 'testrefeshtoken_1',
198
+ usid: 'testusid_1',
199
+ enc_user_id: 'testEncUserId_1',
200
+ id_token: 'testIdToken_1'
201
+ })
202
+ )
203
+ )
204
+ )
205
+
206
+ // submit via Enter key
207
+ await user.keyboard('{Enter}')
208
+
209
+ await waitFor(() => {
210
+ expect(window.location.pathname).toBe('/uk/en-GB/account')
211
+ expect(screen.getByText(/My Profile/i)).toBeInTheDocument()
212
+ })
213
+ })
172
214
  })
173
215
 
174
216
  describe('Error while logging in', function () {
@@ -174,7 +174,8 @@ const ProductDetail = () => {
174
174
  ids: bundleChildProductIds,
175
175
  allImages: false,
176
176
  expand: ['availability', 'variations'],
177
- select: '(data.(id,inventory,master))'
177
+ select: '(data.(id,inventories,inventory,master))',
178
+ ...(selectedInventoryId ? {inventoryIds: selectedInventoryId} : {})
178
179
  }
179
180
  },
180
181
  {
@@ -687,83 +688,73 @@ const ProductDetail = () => {
687
688
  // Render the child products
688
689
  comboProduct.childProducts.map(
689
690
  ({product: childProduct, quantity: childQuantity}) => (
690
- <Island hydrateOn={'visible'} key={childProduct.id}>
691
- <Box data-testid="child-product">
692
- <ProductView
693
- // Do not use an arrow function as we are manipulating the functions scope.
694
- ref={function (ref) {
695
- // Assign the "set" scope of the ref, this is how we access the internal
696
- // validation.
697
- childProductRefs.current[childProduct.id] = {
698
- ref,
699
- validateOrderability:
700
- this.validateOrderability
701
- }
702
- }}
703
- product={childProduct}
704
- isProductPartOfSet={isProductASet}
705
- isProductPartOfBundle={isProductABundle}
706
- childOfBundleQuantity={childQuantity}
707
- selectedBundleParentQuantity={
708
- selectedBundleQuantity
709
- }
710
- addToCart={
711
- isProductASet
712
- ? (productSelectionValues) =>
713
- handleAddToCart(
714
- productSelectionValues
715
- )
716
- : null
691
+ <Box key={childProduct.id} data-testid="child-product">
692
+ <ProductView
693
+ // Do not use an arrow function as we are manipulating the functions scope.
694
+ ref={function (ref) {
695
+ // Assign the "set" scope of the ref, this is how we access the internal
696
+ // validation.
697
+ childProductRefs.current[childProduct.id] = {
698
+ ref,
699
+ validateOrderability: this.validateOrderability
717
700
  }
718
- addToWishlist={
719
- isProductASet ? handleAddToWishlist : null
720
- }
721
- onVariantSelected={(product, variant, quantity) => {
722
- if (quantity) {
723
- setChildProductSelection(
724
- (previousState) => ({
725
- ...previousState,
726
- [product.id]: {
727
- product,
728
- variant,
729
- quantity: isProductABundle
730
- ? childQuantity
731
- : quantity
732
- }
733
- })
734
- )
735
- } else {
736
- const selections = {
737
- ...childProductSelection
701
+ }}
702
+ product={childProduct}
703
+ isProductPartOfSet={isProductASet}
704
+ isProductPartOfBundle={isProductABundle}
705
+ childOfBundleQuantity={childQuantity}
706
+ selectedBundleParentQuantity={selectedBundleQuantity}
707
+ addToCart={
708
+ isProductASet
709
+ ? (productSelectionValues) =>
710
+ handleAddToCart(productSelectionValues)
711
+ : null
712
+ }
713
+ addToWishlist={
714
+ isProductASet ? handleAddToWishlist : null
715
+ }
716
+ onVariantSelected={(product, variant, quantity) => {
717
+ if (quantity) {
718
+ setChildProductSelection((previousState) => ({
719
+ ...previousState,
720
+ [product.id]: {
721
+ product,
722
+ variant,
723
+ quantity: isProductABundle
724
+ ? childQuantity
725
+ : quantity
738
726
  }
739
- delete selections[product.id]
740
- setChildProductSelection(selections)
727
+ }))
728
+ } else {
729
+ const selections = {
730
+ ...childProductSelection
741
731
  }
742
- }}
743
- isProductLoading={isProductLoading}
744
- isBasketLoading={isBasketLoading}
745
- isWishlistLoading={isWishlistLoading}
746
- setChildProductOrderability={
747
- setChildProductOrderability
748
- }
749
- pickupInStore={!!pickupInStoreMap[childProduct?.id]}
750
- setPickupInStore={(checked) =>
751
- childProduct &&
752
- handlePickupInStoreChange(
753
- childProduct.id,
754
- checked
755
- )
732
+ delete selections[product.id]
733
+ setChildProductSelection(selections)
756
734
  }
757
- onOpenStoreLocator={onOpenStoreLocator}
758
- showDeliveryOptions={STORE_LOCATOR_IS_ENABLED}
759
- />
760
- <InformationAccordion product={childProduct} />
761
-
762
- <Box display={['none', 'none', 'none', 'block']}>
763
- <hr />
764
- </Box>
735
+ }}
736
+ isProductLoading={isProductLoading}
737
+ isBasketLoading={isBasketLoading}
738
+ isWishlistLoading={isWishlistLoading}
739
+ setChildProductOrderability={
740
+ setChildProductOrderability
741
+ }
742
+ pickupInStore={!!pickupInStoreMap[childProduct?.id]}
743
+ setPickupInStore={(checked) =>
744
+ childProduct &&
745
+ handlePickupInStoreChange(childProduct.id, checked)
746
+ }
747
+ onOpenStoreLocator={onOpenStoreLocator}
748
+ showDeliveryOptions={
749
+ STORE_LOCATOR_IS_ENABLED && !isProductABundle
750
+ }
751
+ />
752
+ <InformationAccordion product={childProduct} />
753
+
754
+ <Box display={['none', 'none', 'none', 'block']}>
755
+ <hr />
765
756
  </Box>
766
- </Island>
757
+ </Box>
767
758
  )
768
759
  )
769
760
  }
@@ -654,16 +654,26 @@ const ProductList = (props) => {
654
654
  }}
655
655
  dynamicImageProps={{
656
656
  widths: [
657
- '50vw',
658
- '50vw',
659
- '20vw',
660
- '20vw',
661
- '25vw'
657
+ // Each product image can take up the full 50% of the screen width
658
+ '50vw', // base <= 479px
659
+ '50vw', // sm >= 480px ; <= 767px
660
+ // Due to the search refinements panel (fixed 280px), the product images
661
+ // grid doesn't consume the entire screen. The smaller the images get,
662
+ // the more this extra panel impacts the calculation of the responsive
663
+ // image dimensions. Thus, to prevent over-fetching, we define smaller
664
+ // dimensions than the column definitions might suggest. Due to large
665
+ // margins it's also fine to floor the values.
666
+ '15vw' // 15vw is generally a good fit for sizes `md` and above:
667
+ // md >= 768px ; <= 991px | 280px consume ~28-36% of the entire screen | 4 image columns on ~2/3 of the screen ==> ~16vw
668
+ // lg >= 992px ; <= 1279px | 280px consume ~22-28% of the entire screen | 5 image columns on ~3/4 of the screen ==> ~15vw
669
+ // xl >= 1280px ; <= 1535px | 280px consume ~18-22% of the entire screen | 5 image columns on ~4/5 of the screen ==> ~16vw
670
+ // 2xl >= 1536px | 280px consume less than 18% of the screen | 5 image columns on ~5/6 of the screen ==> ~16vw
662
671
  ],
663
- // The first two product images should render eagerly to
664
- // ensure prioritized loading for above-the-fold images
665
- // on mobile.
666
- ...(index < 2
672
+ // For the sake of LCP, load the first three product images
673
+ // eagerly to ensure prioritized loading for all plus one
674
+ // above-the-fold images on mobile and most above-the-fold
675
+ // images on tablet and desktop.
676
+ ...(index < 3
667
677
  ? {
668
678
  imageProps: {
669
679
  loading: 'eager'
@@ -138,25 +138,159 @@ test('should render product list page', async () => {
138
138
  })
139
139
 
140
140
  const helmet = Helmet.peek()
141
- expect(helmet.linkTags).toHaveLength(2)
142
- expect(helmet.linkTags[0]).toStrictEqual({
143
- as: 'image',
144
- rel: 'preload',
145
- href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg',
146
- imageSizes:
147
- '(min-width: 80em) 25vw, (min-width: 62em) 20vw, (min-width: 48em) 20vw, (min-width: 30em) 50vw, 50vw',
148
- imageSrcSet:
149
- 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=198&q=60 198w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=396&q=60 396w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=240&q=60 240w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=480&q=60 480w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=256&q=60 256w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=512&q=60 512w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=384&q=60 384w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=768&q=60 768w'
150
- })
151
- expect(helmet.linkTags[1]).toStrictEqual({
152
- as: 'image',
153
- rel: 'preload',
154
- href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg',
155
- imageSizes:
156
- '(min-width: 80em) 25vw, (min-width: 62em) 20vw, (min-width: 48em) 20vw, (min-width: 30em) 50vw, 50vw',
157
- imageSrcSet:
158
- 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=198&q=60 198w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=396&q=60 396w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=240&q=60 240w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=480&q=60 480w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=256&q=60 256w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=512&q=60 512w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=384&q=60 384w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=768&q=60 768w'
159
- })
141
+ expect(helmet.linkTags).toHaveLength(15) // 3 images with 5 specific preload links with media queries each
142
+ expect(helmet.linkTags).toStrictEqual([
143
+ {
144
+ as: 'image',
145
+ fetchPriority: 'high',
146
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg',
147
+ imageSizes: '50vw',
148
+ imageSrcSet:
149
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=240&q=60 240w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=480&q=60 480w',
150
+ media: '(max-width: 29.99em)',
151
+ rel: 'preload'
152
+ },
153
+ {
154
+ as: 'image',
155
+ fetchPriority: 'high',
156
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg',
157
+ imageSizes: '50vw',
158
+ imageSrcSet:
159
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=384&q=60 384w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=768&q=60 768w',
160
+ media: '(min-width: 30em) and (max-width: 47.99em)',
161
+ rel: 'preload'
162
+ },
163
+ {
164
+ as: 'image',
165
+ fetchPriority: 'high',
166
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg',
167
+ imageSizes: '15vw',
168
+ imageSrcSet:
169
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=149&q=60 149w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=298&q=60 298w',
170
+ media: '(min-width: 48em) and (max-width: 61.99em)',
171
+ rel: 'preload'
172
+ },
173
+ {
174
+ as: 'image',
175
+ fetchPriority: 'high',
176
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg',
177
+ imageSizes: '15vw',
178
+ imageSrcSet:
179
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=192&q=60 192w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=384&q=60 384w',
180
+ media: '(min-width: 62em) and (max-width: 79.99em)',
181
+ rel: 'preload'
182
+ },
183
+ {
184
+ as: 'image',
185
+ fetchPriority: 'high',
186
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg',
187
+ imageSizes: '15vw',
188
+ imageSrcSet:
189
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=230&q=60 230w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw1c5f9222/images/large/PG.10221626.JJ3WCXX.PZ.jpg?sw=460&q=60 460w',
190
+ media: '(min-width: 80em)',
191
+ rel: 'preload'
192
+ },
193
+ {
194
+ as: 'image',
195
+ fetchPriority: 'high',
196
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg',
197
+ imageSizes: '50vw',
198
+ imageSrcSet:
199
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=240&q=60 240w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=480&q=60 480w',
200
+ media: '(max-width: 29.99em)',
201
+ rel: 'preload'
202
+ },
203
+ {
204
+ as: 'image',
205
+ fetchPriority: 'high',
206
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg',
207
+ imageSizes: '50vw',
208
+ imageSrcSet:
209
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=384&q=60 384w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=768&q=60 768w',
210
+ media: '(min-width: 30em) and (max-width: 47.99em)',
211
+ rel: 'preload'
212
+ },
213
+ {
214
+ as: 'image',
215
+ fetchPriority: 'high',
216
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg',
217
+ imageSizes: '15vw',
218
+ imageSrcSet:
219
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=149&q=60 149w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=298&q=60 298w',
220
+ media: '(min-width: 48em) and (max-width: 61.99em)',
221
+ rel: 'preload'
222
+ },
223
+ {
224
+ as: 'image',
225
+ fetchPriority: 'high',
226
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg',
227
+ imageSizes: '15vw',
228
+ imageSrcSet:
229
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=192&q=60 192w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=384&q=60 384w',
230
+ media: '(min-width: 62em) and (max-width: 79.99em)',
231
+ rel: 'preload'
232
+ },
233
+ {
234
+ as: 'image',
235
+ fetchPriority: 'high',
236
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg',
237
+ imageSizes: '15vw',
238
+ imageSrcSet:
239
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=230&q=60 230w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw22e88fa3/images/large/PG.10224484.JJ0CZXX.PZ.jpg?sw=460&q=60 460w',
240
+ media: '(min-width: 80em)',
241
+ rel: 'preload'
242
+ },
243
+ {
244
+ as: 'image',
245
+ fetchPriority: 'high',
246
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwab413b1b/images/large/PG.10243116.JJ2DGXX.PZ.jpg',
247
+ imageSizes: '50vw',
248
+ imageSrcSet:
249
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwab413b1b/images/large/PG.10243116.JJ2DGXX.PZ.jpg?sw=240&q=60 240w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwab413b1b/images/large/PG.10243116.JJ2DGXX.PZ.jpg?sw=480&q=60 480w',
250
+ media: '(max-width: 29.99em)',
251
+ rel: 'preload'
252
+ },
253
+ {
254
+ as: 'image',
255
+ fetchPriority: 'high',
256
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwab413b1b/images/large/PG.10243116.JJ2DGXX.PZ.jpg',
257
+ imageSizes: '50vw',
258
+ imageSrcSet:
259
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwab413b1b/images/large/PG.10243116.JJ2DGXX.PZ.jpg?sw=384&q=60 384w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwab413b1b/images/large/PG.10243116.JJ2DGXX.PZ.jpg?sw=768&q=60 768w',
260
+ media: '(min-width: 30em) and (max-width: 47.99em)',
261
+ rel: 'preload'
262
+ },
263
+ {
264
+ as: 'image',
265
+ fetchPriority: 'high',
266
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwab413b1b/images/large/PG.10243116.JJ2DGXX.PZ.jpg',
267
+ imageSizes: '15vw',
268
+ imageSrcSet:
269
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwab413b1b/images/large/PG.10243116.JJ2DGXX.PZ.jpg?sw=149&q=60 149w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwab413b1b/images/large/PG.10243116.JJ2DGXX.PZ.jpg?sw=298&q=60 298w',
270
+ media: '(min-width: 48em) and (max-width: 61.99em)',
271
+ rel: 'preload'
272
+ },
273
+ {
274
+ as: 'image',
275
+ fetchPriority: 'high',
276
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwab413b1b/images/large/PG.10243116.JJ2DGXX.PZ.jpg',
277
+ imageSizes: '15vw',
278
+ imageSrcSet:
279
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwab413b1b/images/large/PG.10243116.JJ2DGXX.PZ.jpg?sw=192&q=60 192w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwab413b1b/images/large/PG.10243116.JJ2DGXX.PZ.jpg?sw=384&q=60 384w',
280
+ media: '(min-width: 62em) and (max-width: 79.99em)',
281
+ rel: 'preload'
282
+ },
283
+ {
284
+ as: 'image',
285
+ fetchPriority: 'high',
286
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwab413b1b/images/large/PG.10243116.JJ2DGXX.PZ.jpg',
287
+ imageSizes: '15vw',
288
+ imageSrcSet:
289
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwab413b1b/images/large/PG.10243116.JJ2DGXX.PZ.jpg?sw=230&q=60 230w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwab413b1b/images/large/PG.10243116.JJ2DGXX.PZ.jpg?sw=460&q=60 460w',
290
+ media: '(min-width: 80em)',
291
+ rel: 'preload'
292
+ }
293
+ ])
160
294
  })
161
295
 
162
296
  test('should render sort option list page', async () => {
@@ -61,3 +61,32 @@ export function getImageAttributes(props) {
61
61
  }
62
62
  return ensureFetchPriority(imageProps, priority)
63
63
  }
64
+
65
+ /**
66
+ * Utility to return the attributes for a `<link preload>` element that's related to
67
+ * an `<img>` element with the given `props`.
68
+ * @param {{[key: string]: any}} props Image properties
69
+ * @param {('lazy' | 'eager')} [props.loading] Loading strategy
70
+ * @param {('high' | 'low' | 'auto')} [props.fetchPriority] Fetch priority
71
+ * @param {string} [props.sizes] Layout width of the image
72
+ * @param {string} [props.srcSet] One or more image candidate strings, separated using commas
73
+ * @param {string} [props.media] Media query for responsive preloading
74
+ * @param {string} [props.type] MIME type of the resource the element points to
75
+ * @returns {({rel: string, as: string, href: string, fetchPriority?: ('high' | 'low' | 'auto'), media?: string, type?: string, imageSizes?: string, imageSrcSet?: string} | undefined)}
76
+ */
77
+ export function getImageLinkAttributes(props) {
78
+ const loadingStrategy = props?.loading?.toLowerCase?.()
79
+ const fetchPriority = props?.fetchPriority?.toLowerCase?.()
80
+ return fetchPriority === 'high' && (!loadingStrategy || loadingStrategy === 'eager')
81
+ ? {
82
+ rel: 'preload',
83
+ as: 'image',
84
+ href: props.src,
85
+ fetchPriority,
86
+ ...(props.type ? {type: props.type} : {}),
87
+ ...(props.media ? {media: props.media} : {}),
88
+ ...(props.sizes ? {imageSizes: props.sizes} : {}),
89
+ ...(props.srcSet ? {imageSrcSet: props.srcSet} : {})
90
+ }
91
+ : undefined
92
+ }