@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.
- package/CHANGELOG.md +9 -8
- package/app/components/dynamic-image/index.jsx +91 -16
- package/app/components/dynamic-image/index.test.js +214 -30
- package/app/components/image/index.jsx +5 -13
- package/app/components/image/index.test.js +6 -3
- package/app/components/island/README.md +15 -10
- package/app/components/island/index.jsx +12 -5
- package/app/components/island/index.test.js +35 -0
- package/app/components/passwordless-login/index.jsx +4 -5
- package/app/components/passwordless-login/index.test.js +2 -4
- package/app/components/product-tile/index.jsx +1 -1
- package/app/components/product-view-modal/bundle.jsx +12 -2
- package/app/components/social-login/index.jsx +1 -0
- package/app/components/standard-login/index.jsx +4 -1
- package/app/constants.js +3 -0
- package/app/hooks/use-auth-modal.js +68 -67
- package/app/hooks/use-auth-modal.test.js +93 -23
- package/app/hooks/use-datacloud.js +169 -192
- package/app/hooks/use-datacloud.test.js +273 -17
- package/app/pages/cart/index.jsx +2 -1
- package/app/pages/cart/partials/cart-secondary-button-group.jsx +8 -10
- package/app/pages/cart/partials/cart-secondary-button-group.test.js +2 -3
- package/app/pages/checkout/partials/contact-info.jsx +9 -8
- package/app/pages/checkout/partials/contact-info.test.js +41 -4
- package/app/pages/checkout/partials/login-state.jsx +3 -3
- package/app/pages/home/index.test.js +2 -1
- package/app/pages/login/index.jsx +37 -37
- package/app/pages/login/index.test.js +42 -0
- package/app/pages/product-detail/index.jsx +64 -73
- package/app/pages/product-list/index.jsx +19 -9
- package/app/pages/product-list/index.test.js +153 -19
- package/app/utils/image.js +29 -0
- package/app/utils/image.test.js +141 -1
- package/app/utils/responsive-image.js +197 -115
- package/app/utils/responsive-image.test.js +483 -133
- package/config/default.js +2 -2
- package/config/mocks/default.js +2 -2
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
## v7.0.0
|
|
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 [
|
|
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
|
-
-
|
|
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)
|
|
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
|
|
11
|
-
import {
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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 :
|
|
49
|
+
const Component = as ? as : Img
|
|
25
50
|
const theme = useTheme()
|
|
26
51
|
|
|
27
|
-
const responsiveImageProps = useMemo(
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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]).
|
|
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(
|
|
124
|
-
expect(helmet.linkTags
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
'
|
|
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(
|
|
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
|
|
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]).
|
|
197
|
-
expect(
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
|