@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
|
@@ -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
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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={
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
{
|
|
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
|
-
<
|
|
691
|
-
<
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
740
|
-
|
|
727
|
+
}))
|
|
728
|
+
} else {
|
|
729
|
+
const selections = {
|
|
730
|
+
...childProductSelection
|
|
741
731
|
}
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
</
|
|
757
|
+
</Box>
|
|
767
758
|
)
|
|
768
759
|
)
|
|
769
760
|
}
|
|
@@ -654,16 +654,26 @@ const ProductList = (props) => {
|
|
|
654
654
|
}}
|
|
655
655
|
dynamicImageProps={{
|
|
656
656
|
widths: [
|
|
657
|
-
|
|
658
|
-
'50vw',
|
|
659
|
-
'
|
|
660
|
-
|
|
661
|
-
'
|
|
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
|
-
//
|
|
664
|
-
// ensure prioritized loading for
|
|
665
|
-
// on mobile
|
|
666
|
-
|
|
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(
|
|
142
|
-
expect(helmet.linkTags
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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 () => {
|
package/app/utils/image.js
CHANGED
|
@@ -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
|
+
}
|