@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
package/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- ## v7.0.0-dev.0 (May 20, 2025)
1
+ ## v7.0.0 (July 22, 2025)
2
2
 
3
3
  - Improved the layout of product tiles in product scroll and product list [#2446](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2446)
4
4
  - Update the configuration of datacloud [#2467](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2467)
@@ -7,18 +7,19 @@
7
7
  - Fix Einstein event tracking for `addToCart` event [#2558](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2558)
8
8
  - Password Reset and Passwordless Integration Test [#2669](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2669)
9
9
  - Update latest translations for all languages [#2616](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2616)
10
- - Added support for Buy Online Pick up In Store (BOPIS) [#2646](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2646)
10
+ - Added support for Buy Online Pick up In Store (BOPIS) [#2646](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2646) [#2716](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2716) [#2726](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2726) [#2629](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2629) [#2823](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2823)
11
11
  - Load active data scripts on demand only [#2623](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2623)
12
12
  - Provide base image for convenient perf optimizations [#2642](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2642)
13
13
  - Support saving billing phone number on user registration from order confirmation [#2653](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2653)
14
14
  - Support saving default shipping address on user registration from order confirmation [#2706](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2706)
15
15
  - Minor updates to support BOPIS E2E tests [#2716](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2716)
16
- - Provide support for partial hydration [#2696](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2696)
17
- - Show Automatic Bonus Products on Cart Page [#2704](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2704)
18
- - Support Standard Products [#2697](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2697)
16
+ - Provide conditional support for partial hydration (feature flag `PARTIAL_HYDRATION_ENABLED`) [#2696](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2696) [#2846](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2846)
17
+ - Show Automatic Bonus Products on Cart Page [#2704](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2704) [#2760](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2760) [#2815](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2815)
18
+ - [Breaking] Support Standard Products [2697](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2697)
19
19
  - Introduce store locator [#2542](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2542)
20
- - Move product items on cart page into a new component [#2760](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2760)
21
-
20
+ - Fix passwordless race conditions in form submission [#2758](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2758)
21
+ - Use `<picture>` element for responsive images [#2724](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2724)
22
+ - Add Data Cloud partyIdentification events and improve error handling [#2811](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2811)
22
23
 
23
24
  ## v6.1.0 (May 22, 2025)
24
25
 
@@ -456,4 +457,4 @@ The versions published below were not published on npm, and the versioning match
456
457
 
457
458
  ### v1.0.0 (Sep 08, 2021)
458
459
 
459
- - PWA Kit General Availability and open source. 🎉
460
+ - PWA Kit General Availability and open source. 🎉
@@ -1,37 +1,112 @@
1
1
  /*
2
- * Copyright (c) 2021, salesforce.com, inc.
2
+ * Copyright (c) 2025, Salesforce, 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
6
6
  */
7
7
  import React, {useMemo} from 'react'
8
+ import {Helmet} from 'react-helmet'
8
9
  import PropTypes from 'prop-types'
9
10
  import {Box, useTheme} from '@salesforce/retail-react-app/app/components/shared/ui'
10
- import Image from '@salesforce/retail-react-app/app/components/image'
11
- import {getResponsiveImageAttributes} from '@salesforce/retail-react-app/app/utils/responsive-image'
11
+ import {Img} from '@salesforce/retail-react-app/app/components/shared/ui'
12
+ import {getResponsivePictureAttributes} from '@salesforce/retail-react-app/app/utils/responsive-image'
13
+ import {
14
+ getImageAttributes,
15
+ getImageLinkAttributes
16
+ } from '@salesforce/retail-react-app/app/utils/image'
17
+ import {isServer} from '@salesforce/retail-react-app/app/components/image/utils'
12
18
 
13
19
  /**
14
- * Quickly create a responsive image using your Dynamic Imaging Service
15
- * @example
16
- * // Widths without a unit are interpreted as px values
17
- * <DynamicImage src="http://example.com/image.jpg[?sw={width}&q=60]" widths={[100, 360, 720]} />
18
- * <DynamicImage src="http://example.com/image.jpg[?sw={width}&q=60]" widths={{base: 100, sm: 360, md: 720}} />
19
- * // You can also use units of px or vw
20
- * <DynamicImage src="http://example.com/image.jpg[?sw={width}&q=60]" widths={['50vw', '100vw', '500px']} />
20
+ * Responsive image component optimized to work with the Dynamic Imaging Service.
21
+ * Via this component it's easy to create a `<picture>` element with related
22
+ * theme-aware `<source>` elements and responsive preloading for high-priority
23
+ * images.
24
+ * @example Widths without a unit defined as array (interpreted as px values)
25
+ * <DynamicImage
26
+ * src="http://example.com/image.jpg[?sw={width}&q=60]"
27
+ * widths={[100, 360, 720]} />
28
+ * @example Widths without a unit defined as object (interpreted as px values)
29
+ * <DynamicImage
30
+ * src="http://example.com/image.jpg[?sw={width}&q=60]"
31
+ * widths={{base: 100, sm: 360, md: 720}} />
32
+ * @example Widths with mixed px and vw units defined as array
33
+ * <DynamicImage
34
+ * src="http://example.com/image.jpg[?sw={width}&q=60]"
35
+ * widths={['50vw', '100vw', '500px']} />
36
+ * @example Eagerly load image with high priority and responsive preloading
37
+ * <DynamicImage
38
+ * src="http://example.com/image.jpg[?sw={width}&q=60]"
39
+ * widths={['50vw', '50vw', '20vw', '20vw', '25vw']}
40
+ * imageProps={{loading: 'eager'}}
41
+ * />
42
+ * @see {@link https://web.dev/learn/design/responsive-images}
43
+ * @see {@link https://web.dev/learn/design/picture-element}
44
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/picture}
45
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Responsive_images}
21
46
  * @see {@link https://help.salesforce.com/s/articleView?id=cc.b2c_image_transformation_service.htm&type=5}
22
47
  */
23
48
  const DynamicImage = ({src, widths, imageProps, as, ...rest}) => {
24
- const Component = as ? as : Image
49
+ const Component = as ? as : Img
25
50
  const theme = useTheme()
26
51
 
27
- const responsiveImageProps = useMemo(
28
- () => getResponsiveImageAttributes({src, widths, breakpoints: theme.breakpoints}),
29
- [src, widths, theme.breakpoints]
30
- )
52
+ const [responsiveImageProps, numSources, effectiveImageProps, responsiveLinks] = useMemo(() => {
53
+ const responsiveImageProps = getResponsivePictureAttributes({
54
+ src,
55
+ widths,
56
+ breakpoints: theme.breakpoints
57
+ })
58
+ const effectiveImageProps = getImageAttributes(imageProps)
59
+ const fetchPriority = effectiveImageProps.fetchPriority
60
+ const responsiveLinks =
61
+ !responsiveImageProps.links.length && fetchPriority === 'high'
62
+ ? [
63
+ getImageLinkAttributes({
64
+ ...effectiveImageProps,
65
+ fetchPriority, // React <18 vs. >=19 issue
66
+ src: responsiveImageProps.src
67
+ })
68
+ ]
69
+ : responsiveImageProps.links.reduce((acc, link) => {
70
+ const linkProps = getImageLinkAttributes({
71
+ ...effectiveImageProps,
72
+ ...link,
73
+ fetchPriority, // React <18 vs. >=19 issue
74
+ src: responsiveImageProps.src
75
+ })
76
+ if (linkProps) {
77
+ acc.push(linkProps)
78
+ }
79
+ return acc
80
+ }, [])
81
+ return [
82
+ responsiveImageProps,
83
+ responsiveImageProps.sources.length,
84
+ effectiveImageProps,
85
+ responsiveLinks
86
+ ]
87
+ }, [src, widths, theme.breakpoints])
31
88
 
32
89
  return (
33
90
  <Box {...rest}>
34
- <Component {...responsiveImageProps} {...imageProps} />
91
+ {numSources > 0 ? (
92
+ <picture>
93
+ {responsiveImageProps.sources.map(({srcSet, sizes, media}, idx) => (
94
+ <source key={idx} media={media} sizes={sizes} srcSet={srcSet} />
95
+ ))}
96
+ <Component {...effectiveImageProps} src={responsiveImageProps.src} />
97
+ </picture>
98
+ ) : (
99
+ <Component {...effectiveImageProps} src={responsiveImageProps.src} />
100
+ )}
101
+
102
+ {isServer() && responsiveLinks.length > 0 && (
103
+ <Helmet>
104
+ {responsiveLinks.map((responsiveLinkProps, idx) => {
105
+ const {href, ...rest} = responsiveLinkProps
106
+ return <link key={idx} {...rest} href={href} />
107
+ })}
108
+ </Helmet>
109
+ )}
35
110
  </Box>
36
111
  )
37
112
  }
@@ -7,7 +7,7 @@
7
7
  /* eslint-disable jest/no-conditional-expect */
8
8
  import React from 'react'
9
9
  import {Helmet} from 'react-helmet'
10
- import DynamicImage from '@salesforce/retail-react-app/app/components/dynamic-image/index'
10
+ import DynamicImage from '@salesforce/retail-react-app/app/components/dynamic-image'
11
11
  import {Img} from '@salesforce/retail-react-app/app/components/shared/ui'
12
12
  import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
13
13
  import {isServer} from '@salesforce/retail-react-app/app/components/image/utils'
@@ -26,19 +26,23 @@ const imageProps = {
26
26
 
27
27
  describe('Dynamic Image Component', () => {
28
28
  test('renders an image without decoding strategy and fetch priority', () => {
29
- const {getAllByTitle} = renderWithProviders(
30
- <DynamicImage src={src} imageProps={imageProps} />
29
+ const {getByTestId, getAllByTitle} = renderWithProviders(
30
+ <DynamicImage data-testid={'dynamic-image'} src={src} imageProps={imageProps} />
31
31
  )
32
+
33
+ const wrapper = getByTestId('dynamic-image')
32
34
  const elements = getAllByTitle(imageProps.title)
33
35
  expect(elements).toHaveLength(1)
34
36
  expect(elements[0]).not.toHaveAttribute('decoding')
35
37
  expect(elements[0]).not.toHaveAttribute('fetchpriority')
38
+ expect(wrapper.firstElementChild).toBe(elements[0])
36
39
  })
37
40
 
38
41
  describe('loading="lazy"', () => {
39
42
  test('renders an image using the default "async" decoding strategy', () => {
40
- const {getAllByTitle} = renderWithProviders(
43
+ const {getByTestId, getAllByTitle} = renderWithProviders(
41
44
  <DynamicImage
45
+ data-testid={'dynamic-image'}
42
46
  src={src}
43
47
  imageProps={{
44
48
  ...imageProps,
@@ -46,16 +50,20 @@ describe('Dynamic Image Component', () => {
46
50
  }}
47
51
  />
48
52
  )
53
+
54
+ const wrapper = getByTestId('dynamic-image')
49
55
  const elements = getAllByTitle(imageProps.title)
50
56
  expect(elements).toHaveLength(1)
51
57
  expect(elements[0]).toHaveAttribute('decoding', 'async')
58
+ expect(wrapper.firstElementChild).toBe(elements[0])
52
59
  })
53
60
 
54
61
  test.each(['sync', 'async', 'auto'])(
55
62
  'renders an image using an explicit "%s" decoding strategy',
56
63
  (decoding) => {
57
- const {getAllByTitle} = renderWithProviders(
64
+ const {getByTestId, getAllByTitle} = renderWithProviders(
58
65
  <DynamicImage
66
+ data-testid={'dynamic-image'}
59
67
  src={src}
60
68
  imageProps={{
61
69
  ...imageProps,
@@ -64,15 +72,19 @@ describe('Dynamic Image Component', () => {
64
72
  }}
65
73
  />
66
74
  )
75
+
76
+ const wrapper = getByTestId('dynamic-image')
67
77
  const elements = getAllByTitle(imageProps.title)
68
78
  expect(elements).toHaveLength(1)
69
79
  expect(elements[0]).toHaveAttribute('decoding', decoding)
80
+ expect(wrapper.firstElementChild).toBe(elements[0])
70
81
  }
71
82
  )
72
83
 
73
84
  test('renders an image replacing an invalid decoding strategy with the default "async" value', () => {
74
- const {getAllByTitle} = renderWithProviders(
85
+ const {getByTestId, getAllByTitle} = renderWithProviders(
75
86
  <DynamicImage
87
+ data-testid={'dynamic-image'}
76
88
  src={src}
77
89
  imageProps={{
78
90
  ...imageProps,
@@ -81,14 +93,17 @@ describe('Dynamic Image Component', () => {
81
93
  }}
82
94
  />
83
95
  )
96
+ const wrapper = getByTestId('dynamic-image')
84
97
  const elements = getAllByTitle(imageProps.title)
85
98
  expect(elements).toHaveLength(1)
86
99
  expect(elements[0]).toHaveAttribute('decoding', 'async')
100
+ expect(wrapper.firstElementChild).toBe(elements[0])
87
101
  })
88
102
 
89
- test('renders an explicitly given image component without attribute modifications', () => {
90
- const {getAllByTitle} = renderWithProviders(
103
+ test('renders an explicitly given image component', () => {
104
+ const {getByTestId, getAllByTitle} = renderWithProviders(
91
105
  <DynamicImage
106
+ data-testid={'dynamic-image'}
92
107
  src={src}
93
108
  as={Img}
94
109
  imageProps={{
@@ -97,16 +112,81 @@ describe('Dynamic Image Component', () => {
97
112
  }}
98
113
  />
99
114
  )
115
+
116
+ const wrapper = getByTestId('dynamic-image')
100
117
  const elements = getAllByTitle(imageProps.title)
101
118
  expect(elements).toHaveLength(1)
102
- expect(elements[0]).not.toHaveAttribute('decoding')
119
+ expect(elements[0]).toHaveAttribute('decoding', 'async')
120
+ expect(wrapper.firstElementChild).toBe(elements[0])
121
+ })
122
+
123
+ test('renders an image with explicit widths', () => {
124
+ const {getByTestId, getAllByTitle} = renderWithProviders(
125
+ <DynamicImage
126
+ data-testid={'dynamic-image'}
127
+ src={src}
128
+ imageProps={{
129
+ ...imageProps,
130
+ loading: 'lazy'
131
+ }}
132
+ widths={['50vw', '50vw', '20vw', '20vw', '25vw']}
133
+ />
134
+ )
135
+
136
+ const wrapper = getByTestId('dynamic-image')
137
+ const elements = getAllByTitle(imageProps.title)
138
+ expect(elements).toHaveLength(1)
139
+ expect(elements[0]).toHaveAttribute('src', src)
140
+ expect(elements[0]).toHaveAttribute('loading', 'lazy')
141
+ expect(elements[0]).toHaveAttribute('decoding', 'async')
142
+ expect(elements[0]).not.toHaveAttribute('sizes')
143
+ expect(elements[0]).not.toHaveAttribute('srcset')
144
+
145
+ expect(wrapper.firstElementChild).not.toBe(elements[0])
146
+ expect(wrapper.firstElementChild.tagName.toLowerCase()).toBe('picture')
147
+
148
+ const sourceElements = Array.from(wrapper.querySelectorAll('source'))
149
+ expect(sourceElements).toHaveLength(5)
150
+ expect(sourceElements[0]).toHaveAttribute('media', '(min-width: 80em)')
151
+ expect(sourceElements[0]).toHaveAttribute('sizes', '25vw')
152
+ expect(sourceElements[0]).toHaveAttribute(
153
+ 'srcset',
154
+ [384, 768].map((width) => `${src} ${width}w`).join(', ')
155
+ )
156
+ expect(sourceElements[1]).toHaveAttribute('media', '(min-width: 62em)')
157
+ expect(sourceElements[1]).toHaveAttribute('sizes', '20vw')
158
+ expect(sourceElements[1]).toHaveAttribute(
159
+ 'srcset',
160
+ [256, 512].map((width) => `${src} ${width}w`).join(', ')
161
+ )
162
+ expect(sourceElements[2]).toHaveAttribute('media', '(min-width: 48em)')
163
+ expect(sourceElements[2]).toHaveAttribute('sizes', '20vw')
164
+ expect(sourceElements[2]).toHaveAttribute(
165
+ 'srcset',
166
+ [198, 396].map((width) => `${src} ${width}w`).join(', ')
167
+ )
168
+ expect(sourceElements[3]).toHaveAttribute('media', '(min-width: 30em)')
169
+ expect(sourceElements[3]).toHaveAttribute('sizes', '50vw')
170
+ expect(sourceElements[3]).toHaveAttribute(
171
+ 'srcset',
172
+ [384, 768].map((width) => `${src} ${width}w`).join(', ')
173
+ )
174
+ expect(sourceElements[4]).not.toHaveAttribute('media')
175
+ expect(sourceElements[4]).toHaveAttribute('sizes', '50vw')
176
+ expect(sourceElements[4]).toHaveAttribute(
177
+ 'srcset',
178
+ [240, 480].map((width) => `${src} ${width}w`).join(', ')
179
+ )
180
+
181
+ expect(Helmet.peek()?.linkTags ?? []).toStrictEqual([])
103
182
  })
104
183
  })
105
184
 
106
185
  describe('loading="eager"', () => {
107
186
  test('renders an image using the default "high" fetch priority', () => {
108
- const {getAllByTitle} = renderWithProviders(
187
+ const {getByTestId, getAllByTitle} = renderWithProviders(
109
188
  <DynamicImage
189
+ data-testid={'dynamic-image'}
110
190
  src={src}
111
191
  imageProps={{
112
192
  ...imageProps,
@@ -115,28 +195,109 @@ describe('Dynamic Image Component', () => {
115
195
  widths={['50vw', '50vw', '20vw', '20vw', '25vw']}
116
196
  />
117
197
  )
198
+
199
+ const wrapper = getByTestId('dynamic-image')
118
200
  const elements = getAllByTitle(imageProps.title)
119
201
  expect(elements).toHaveLength(1)
202
+ expect(elements[0]).toHaveAttribute('src', src)
203
+ expect(elements[0]).toHaveAttribute('loading', 'eager')
120
204
  expect(elements[0]).toHaveAttribute('fetchpriority', 'high')
205
+ expect(elements[0]).not.toHaveAttribute('sizes')
206
+ expect(elements[0]).not.toHaveAttribute('srcset')
207
+
208
+ expect(wrapper.firstElementChild).not.toBe(elements[0])
209
+ expect(wrapper.firstElementChild.tagName.toLowerCase()).toBe('picture')
210
+
211
+ const sourceElements = Array.from(wrapper.querySelectorAll('source'))
212
+ expect(sourceElements).toHaveLength(5)
213
+ expect(sourceElements[0]).toHaveAttribute('media', '(min-width: 80em)')
214
+ expect(sourceElements[0]).toHaveAttribute('sizes', '25vw')
215
+ expect(sourceElements[0]).toHaveAttribute(
216
+ 'srcset',
217
+ [384, 768].map((width) => `${src} ${width}w`).join(', ')
218
+ )
219
+ expect(sourceElements[1]).toHaveAttribute('media', '(min-width: 62em)')
220
+ expect(sourceElements[1]).toHaveAttribute('sizes', '20vw')
221
+ expect(sourceElements[1]).toHaveAttribute(
222
+ 'srcset',
223
+ [256, 512].map((width) => `${src} ${width}w`).join(', ')
224
+ )
225
+ expect(sourceElements[2]).toHaveAttribute('media', '(min-width: 48em)')
226
+ expect(sourceElements[2]).toHaveAttribute('sizes', '20vw')
227
+ expect(sourceElements[2]).toHaveAttribute(
228
+ 'srcset',
229
+ [198, 396].map((width) => `${src} ${width}w`).join(', ')
230
+ )
231
+ expect(sourceElements[3]).toHaveAttribute('media', '(min-width: 30em)')
232
+ expect(sourceElements[3]).toHaveAttribute('sizes', '50vw')
233
+ expect(sourceElements[3]).toHaveAttribute(
234
+ 'srcset',
235
+ [384, 768].map((width) => `${src} ${width}w`).join(', ')
236
+ )
237
+ expect(sourceElements[4]).not.toHaveAttribute('media')
238
+ expect(sourceElements[4]).toHaveAttribute('sizes', '50vw')
239
+ expect(sourceElements[4]).toHaveAttribute(
240
+ 'srcset',
241
+ [240, 480].map((width) => `${src} ${width}w`).join(', ')
242
+ )
121
243
 
122
244
  const helmet = Helmet.peek()
123
- expect(helmet.linkTags).toHaveLength(1)
124
- expect(helmet.linkTags[0]).toStrictEqual({
125
- as: 'image',
126
- href: src,
127
- rel: 'preload',
128
- imageSizes:
129
- '(min-width: 80em) 25vw, (min-width: 62em) 20vw, (min-width: 48em) 20vw, (min-width: 30em) 50vw, 50vw',
130
- imageSrcSet:
131
- 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 198w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 396w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 240w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 480w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 256w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 512w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 384w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 768w'
132
- })
245
+ expect(helmet.linkTags).toHaveLength(5)
246
+ expect(helmet.linkTags).toStrictEqual([
247
+ {
248
+ rel: 'preload',
249
+ as: 'image',
250
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg',
251
+ fetchPriority: 'high',
252
+ media: '(max-width: 29.99em)',
253
+ imageSizes: '50vw',
254
+ imageSrcSet: [240, 480].map((width) => `${src} ${width}w`).join(', ')
255
+ },
256
+ {
257
+ rel: 'preload',
258
+ as: 'image',
259
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg',
260
+ fetchPriority: 'high',
261
+ media: '(min-width: 30em) and (max-width: 47.99em)',
262
+ imageSizes: '50vw',
263
+ imageSrcSet: [384, 768].map((width) => `${src} ${width}w`).join(', ')
264
+ },
265
+ {
266
+ rel: 'preload',
267
+ as: 'image',
268
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg',
269
+ fetchPriority: 'high',
270
+ media: '(min-width: 48em) and (max-width: 61.99em)',
271
+ imageSizes: '20vw',
272
+ imageSrcSet: [198, 396].map((width) => `${src} ${width}w`).join(', ')
273
+ },
274
+ {
275
+ rel: 'preload',
276
+ as: 'image',
277
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg',
278
+ fetchPriority: 'high',
279
+ media: '(min-width: 62em) and (max-width: 79.99em)',
280
+ imageSizes: '20vw',
281
+ imageSrcSet: [256, 512].map((width) => `${src} ${width}w`).join(', ')
282
+ },
283
+ {
284
+ rel: 'preload',
285
+ as: 'image',
286
+ href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg',
287
+ fetchPriority: 'high',
288
+ media: '(min-width: 80em)',
289
+ imageSizes: '25vw',
290
+ imageSrcSet: [384, 768].map((width) => `${src} ${width}w`).join(', ')
291
+ }
292
+ ])
133
293
  })
134
294
 
135
295
  test.each(['high', 'low', 'auto'])(
136
296
  'renders an image using an explicit "%s" fetch priority',
137
297
  (fetchPriority) => {
138
- const {getAllByTitle} = renderWithProviders(
298
+ const {getByTestId, getAllByTitle} = renderWithProviders(
139
299
  <DynamicImage
300
+ data-testid={'dynamic-image'}
140
301
  src={src}
141
302
  imageProps={{
142
303
  ...imageProps,
@@ -145,9 +306,12 @@ describe('Dynamic Image Component', () => {
145
306
  }}
146
307
  />
147
308
  )
309
+
310
+ const wrapper = getByTestId('dynamic-image')
148
311
  const elements = getAllByTitle(imageProps.title)
149
312
  expect(elements).toHaveLength(1)
150
313
  expect(elements[0]).toHaveAttribute('fetchpriority', fetchPriority)
314
+ expect(wrapper.firstElementChild).toBe(elements[0])
151
315
 
152
316
  const helmet = Helmet.peek()
153
317
  if (fetchPriority === 'high') {
@@ -155,7 +319,8 @@ describe('Dynamic Image Component', () => {
155
319
  expect(helmet.linkTags[0]).toStrictEqual({
156
320
  as: 'image',
157
321
  href: src,
158
- rel: 'preload'
322
+ rel: 'preload',
323
+ fetchPriority: 'high'
159
324
  })
160
325
  } else {
161
326
  expect(helmet.linkTags).toStrictEqual([])
@@ -164,8 +329,9 @@ describe('Dynamic Image Component', () => {
164
329
  )
165
330
 
166
331
  test('renders an image replacing an invalid fetch priority with the default "auto" value', () => {
167
- const {getAllByTitle} = renderWithProviders(
332
+ const {getByTestId, getAllByTitle} = renderWithProviders(
168
333
  <DynamicImage
334
+ data-testid={'dynamic-image'}
169
335
  src={src}
170
336
  imageProps={{
171
337
  ...imageProps,
@@ -174,15 +340,19 @@ describe('Dynamic Image Component', () => {
174
340
  }}
175
341
  />
176
342
  )
343
+
344
+ const wrapper = getByTestId('dynamic-image')
177
345
  const elements = getAllByTitle(imageProps.title)
178
346
  expect(elements).toHaveLength(1)
179
347
  expect(elements[0]).toHaveAttribute('fetchpriority', 'auto')
180
- expect(Helmet.peek().linkTags).toStrictEqual([])
348
+ expect(wrapper.firstElementChild).toBe(elements[0])
349
+ expect(Helmet.peek()?.linkTags ?? []).toStrictEqual([])
181
350
  })
182
351
 
183
- test('renders an explicitly given image component without modifications', () => {
184
- const {getAllByTitle} = renderWithProviders(
352
+ test('renders an explicitly given image component', () => {
353
+ const {getByTestId, getAllByTitle} = renderWithProviders(
185
354
  <DynamicImage
355
+ data-testid={'dynamic-image'}
186
356
  src={src}
187
357
  as={Img}
188
358
  imageProps={{
@@ -191,16 +361,27 @@ describe('Dynamic Image Component', () => {
191
361
  }}
192
362
  />
193
363
  )
364
+
365
+ const wrapper = getByTestId('dynamic-image')
194
366
  const elements = getAllByTitle(imageProps.title)
195
367
  expect(elements).toHaveLength(1)
196
- expect(elements[0]).not.toHaveAttribute('fetchpriority')
197
- expect(Helmet.peek().linkTags).toStrictEqual([])
368
+ expect(elements[0]).toHaveAttribute('fetchpriority', 'high')
369
+ expect(wrapper.firstElementChild).toBe(elements[0])
370
+ expect(Helmet.peek().linkTags).toStrictEqual([
371
+ {
372
+ as: 'image',
373
+ href: src,
374
+ rel: 'preload',
375
+ fetchPriority: 'high'
376
+ }
377
+ ])
198
378
  })
199
379
 
200
380
  test('renders an image on the client', () => {
201
381
  isServer.mockReturnValue(false)
202
- const {getAllByTitle} = renderWithProviders(
382
+ const {getByTestId, getAllByTitle} = renderWithProviders(
203
383
  <DynamicImage
384
+ data-testid={'dynamic-image'}
204
385
  src={src}
205
386
  imageProps={{
206
387
  ...imageProps,
@@ -208,10 +389,13 @@ describe('Dynamic Image Component', () => {
208
389
  }}
209
390
  />
210
391
  )
392
+
393
+ const wrapper = getByTestId('dynamic-image')
211
394
  const elements = getAllByTitle(imageProps.title)
212
395
  expect(elements).toHaveLength(1)
213
396
  expect(elements[0]).toHaveAttribute('fetchpriority', 'high')
214
- expect(Helmet.peek().linkTags).toStrictEqual([])
397
+ expect(wrapper.firstElementChild).toBe(elements[0])
398
+ expect(Helmet.peek()?.linkTags ?? []).toStrictEqual([])
215
399
  })
216
400
  })
217
401
  })
@@ -8,7 +8,10 @@ import React, {useMemo} from 'react'
8
8
  import {Helmet} from 'react-helmet'
9
9
  import PropTypes from 'prop-types'
10
10
  import {Img} from '@salesforce/retail-react-app/app/components/shared/ui'
11
- import {getImageAttributes} from '@salesforce/retail-react-app/app/utils/image'
11
+ import {
12
+ getImageAttributes,
13
+ getImageLinkAttributes
14
+ } from '@salesforce/retail-react-app/app/utils/image'
12
15
  import {isServer} from '@salesforce/retail-react-app/app/components/image/utils'
13
16
 
14
17
  /**
@@ -24,18 +27,7 @@ const Image = (props) => {
24
27
  const Component = as ? as : Img
25
28
  const [effectiveImageProps, effectiveLinkProps] = useMemo(() => {
26
29
  const imageProps = getImageAttributes(rest)
27
- const loadingStrategy = imageProps?.loading?.toLowerCase?.()
28
- const fetchPriority = imageProps?.fetchPriority?.toLowerCase?.()
29
- const linkProps =
30
- fetchPriority === 'high' && (!loadingStrategy || loadingStrategy === 'eager')
31
- ? {
32
- rel: 'preload',
33
- as: 'image',
34
- href: imageProps.src,
35
- ...(imageProps.sizes ? {imageSizes: imageProps.sizes} : {}),
36
- ...(imageProps.srcSet ? {imageSrcSet: imageProps.srcSet} : {})
37
- }
38
- : undefined
30
+ const linkProps = getImageLinkAttributes(imageProps)
39
31
  return [imageProps, linkProps]
40
32
  }, [rest])
41
33
 
@@ -83,7 +83,8 @@ describe('Image Component', () => {
83
83
  expect(helmet.linkTags[0]).toStrictEqual({
84
84
  as: 'image',
85
85
  href: imageProps.src,
86
- rel: 'preload'
86
+ rel: 'preload',
87
+ fetchPriority: 'high'
87
88
  })
88
89
  })
89
90
 
@@ -103,7 +104,8 @@ describe('Image Component', () => {
103
104
  expect(helmet.linkTags[0]).toStrictEqual({
104
105
  as: 'image',
105
106
  href: imageProps.src,
106
- rel: 'preload'
107
+ rel: 'preload',
108
+ fetchPriority: 'high'
107
109
  })
108
110
  } else {
109
111
  expect(helmet.linkTags).toStrictEqual([])
@@ -134,7 +136,8 @@ describe('Image Component', () => {
134
136
  expect(helmet.linkTags[0]).toStrictEqual({
135
137
  as: 'image',
136
138
  href: imageProps.src,
137
- rel: 'preload'
139
+ rel: 'preload',
140
+ fetchPriority: 'high'
138
141
  })
139
142
  })
140
143