@salesforce/retail-react-app 6.0.0 → 6.1.0-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## v6.1.0-dev (Feb 18, 2025)
2
+
3
+ - Fix hreflang alternate links [#2269](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2269)
4
+ - PDP / PLP: Add page meta data tags that have been defined in BM [#2232](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2232)
5
+ - Send PWA Kit events to Data Cloud [#318] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2229)
6
+ - Fix dependencies vulnerabilities [#2338](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2338)
7
+
8
+ ## v6.0.1 (Mar 05, 2025)
9
+ - Update PWA-Kit SDKs to v3.9.1 [#2301](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2301)
10
+
1
11
  ## v6.0.0 (Feb 18, 2025)
2
12
 
3
13
  - DNT Consent Banner: [#2203](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2203)
@@ -80,6 +80,7 @@ import {
80
80
 
81
81
  import Seo from '@salesforce/retail-react-app/app/components/seo'
82
82
  import {Helmet} from 'react-helmet'
83
+ import {getPathWithLocale} from '@salesforce/retail-react-app/app/utils/url'
83
84
 
84
85
  const PlaceholderComponent = () => (
85
86
  <Center p="2">
@@ -336,7 +337,12 @@ const App = (props) => {
336
337
  <link
337
338
  rel="alternate"
338
339
  hrefLang={locale.id.toLowerCase()}
339
- href={`${appOrigin}${buildUrl(location.pathname)}`}
340
+ href={`${appOrigin}${getPathWithLocale(locale.id, buildUrl, {
341
+ location: {
342
+ ...location,
343
+ search: ''
344
+ }
345
+ })}`}
340
346
  key={locale.id}
341
347
  />
342
348
  ))}
@@ -344,7 +350,12 @@ const App = (props) => {
344
350
  <link
345
351
  rel="alternate"
346
352
  hrefLang={site.l10n.defaultLocale.slice(0, 2)}
347
- href={`${appOrigin}${buildUrl(location.pathname)}`}
353
+ href={`${appOrigin}${getPathWithLocale(locale.id, buildUrl, {
354
+ location: {
355
+ ...location,
356
+ search: ''
357
+ }
358
+ })}`}
348
359
  />
349
360
  {/* A wider fallback for user locales that the app does not support */}
350
361
  <link rel="alternate" hrefLang="x-default" href={`${appOrigin}/`} />
@@ -16,7 +16,8 @@ import {DEFAULT_LOCALE} from '@salesforce/retail-react-app/app/utils/test-utils'
16
16
  import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site'
17
17
  import messages from '@salesforce/retail-react-app/app/static/translations/compiled/en-GB.json'
18
18
  import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
19
- import * as constants from '@salesforce/retail-react-app/app/constants'
19
+ import {prependHandlersToServer} from '@salesforce/retail-react-app/jest-setup'
20
+ import {mockCustomerBaskets} from '@salesforce/retail-react-app/app/mocks/mock-data'
20
21
 
21
22
  jest.mock('../../hooks/use-multi-site', () => jest.fn())
22
23
  jest.mock('../../hooks/use-update-shopper-context', () => ({
@@ -24,29 +25,9 @@ jest.mock('../../hooks/use-update-shopper-context', () => ({
24
25
  }))
25
26
 
26
27
  let windowSpy
27
- let originalValue
28
- beforeAll(() => {
29
- jest.spyOn(console, 'log').mockImplementation(jest.fn())
30
- jest.spyOn(console, 'groupCollapsed').mockImplementation(jest.fn())
31
- originalValue = constants.ACTIVE_DATA_ENABLED
32
- })
33
-
34
- afterAll(() => {
35
- console.log.mockRestore()
36
- console.groupCollapsed.mockRestore()
37
- constants.ACTIVE_DATA_ENABLED = originalValue
38
- })
39
- beforeEach(() => {
40
- windowSpy = jest.spyOn(window, 'window', 'get')
41
- })
42
-
43
- afterEach(() => {
44
- console.log.mockClear()
45
- console.groupCollapsed.mockClear()
46
- windowSpy.mockRestore()
47
- })
48
28
 
49
29
  const mockUpdateDNT = jest.fn()
30
+ const mockActiveDataFlag = jest.fn()
50
31
  jest.mock('@salesforce/commerce-sdk-react', () => {
51
32
  const originalModule = jest.requireActual('@salesforce/commerce-sdk-react')
52
33
  return {
@@ -55,6 +36,40 @@ jest.mock('@salesforce/commerce-sdk-react', () => {
55
36
  }
56
37
  })
57
38
 
39
+ jest.mock('@salesforce/retail-react-app/app/constants', () => {
40
+ const originalModule = jest.requireActual('@salesforce/retail-react-app/app/constants')
41
+ return {
42
+ ...originalModule,
43
+ get ACTIVE_DATA_ENABLED() {
44
+ return mockActiveDataFlag()
45
+ }
46
+ }
47
+ })
48
+ beforeEach(() => {
49
+ windowSpy = jest.spyOn(window, 'window', 'get')
50
+ mockActiveDataFlag.mockReturnValue(true)
51
+ prependHandlersToServer([
52
+ {
53
+ path: '*/baskets/:basketId/customer',
54
+ method: 'put',
55
+ res: () => {
56
+ return {
57
+ ...mockCustomerBaskets.baskets[0],
58
+ customerInfo: {
59
+ customerId: 'abmuc2wupJxeoRxuo3wqYYmbhI',
60
+ email: 'shopperUpdate@salesforce-test.com'
61
+ }
62
+ }
63
+ }
64
+ }
65
+ ])
66
+ })
67
+
68
+ afterEach(() => {
69
+ windowSpy.mockRestore()
70
+ jest.restoreAllMocks()
71
+ jest.resetModules()
72
+ })
58
73
  describe('App', () => {
59
74
  const site = {
60
75
  ...mockConfig.app.sites[0],
@@ -89,7 +104,7 @@ describe('App', () => {
89
104
  })
90
105
 
91
106
  test('Active Data component is not rendered', async () => {
92
- constants.ACTIVE_DATA_ENABLED = false
107
+ mockActiveDataFlag.mockImplementation(() => false)
93
108
  useMultiSite.mockImplementation(() => resultUseMultiSite)
94
109
  renderWithProviders(
95
110
  <App targetLocale={DEFAULT_LOCALE} defaultLocale={DEFAULT_LOCALE} messages={messages}>
@@ -105,17 +120,18 @@ describe('App', () => {
105
120
  })
106
121
 
107
122
  test('Active Data component is rendered appropriately', async () => {
108
- constants.ACTIVE_DATA_ENABLED = true
109
123
  useMultiSite.mockImplementation(() => resultUseMultiSite)
110
124
  renderWithProviders(
111
125
  <App targetLocale={DEFAULT_LOCALE} defaultLocale={DEFAULT_LOCALE} messages={messages}>
112
126
  <p>Any children here</p>
113
127
  </App>
114
128
  )
115
- await waitFor(() => expect(document.getElementById('headActiveData')).toBeInTheDocument())
116
- await waitFor(() => expect(document.getElementById('dwanalytics')).toBeInTheDocument())
117
- await waitFor(() => expect(document.getElementById('dwac')).toBeInTheDocument())
118
- expect(screen.getByText('Any children here')).toBeInTheDocument()
129
+ await waitFor(() => {
130
+ expect(document.getElementById('headActiveData')).toBeInTheDocument()
131
+ expect(document.getElementById('dwanalytics')).toBeInTheDocument()
132
+ expect(document.getElementById('dwac')).toBeInTheDocument()
133
+ expect(screen.getByText('Any children here')).toBeInTheDocument()
134
+ })
119
135
  })
120
136
 
121
137
  test('The localized hreflang links exist in the html head', () => {
@@ -124,11 +140,39 @@ describe('App', () => {
124
140
  <App targetLocale={DEFAULT_LOCALE} defaultLocale={DEFAULT_LOCALE} messages={messages} />
125
141
  )
126
142
 
143
+ // expected locales for hrefLang
144
+ const hrefLangLocales = mockConfig.app.sites[0].l10n.supportedLocales.map(
145
+ (locale) => locale.id
146
+ )
127
147
  const helmet = Helmet.peek()
128
148
  const hreflangLinks = helmet.linkTags.filter((link) => link.rel === 'alternate')
129
-
130
149
  const hasGeneralLocale = ({hrefLang}) => hrefLang === DEFAULT_LOCALE.slice(0, 2)
131
150
 
151
+ hrefLangLocales.forEach((supportedLocale) => {
152
+ expect(
153
+ hreflangLinks.some(
154
+ (link) => link.hrefLang.toLowerCase() === supportedLocale.toLowerCase()
155
+ )
156
+ ).toBe(true)
157
+ expect(hreflangLinks.some((link) => hasGeneralLocale(link))).toBe(true)
158
+ })
159
+
160
+ // localeRefs takes locale alias into consideration
161
+ const localeRefs = mockConfig.app.sites[0].l10n.supportedLocales.map(
162
+ (locale) => locale.alias || locale.id
163
+ )
164
+
165
+ localeRefs.forEach((localeRef) => {
166
+ expect(hreflangLinks.some((link) => link.href.includes(localeRef))).toBe(true)
167
+ // expecting href does not contain search query params in the href since it is a canonical url
168
+ expect(
169
+ hreflangLinks.some((link) => {
170
+ const urlObj = new URL(link.href)
171
+ return urlObj.search.length > 0
172
+ })
173
+ ).toBe(false)
174
+ })
175
+
132
176
  // `length + 2` because one for a general locale and the other with x-default value
133
177
  expect(hreflangLinks).toHaveLength(resultUseMultiSite.site.l10n.supportedLocales.length + 2)
134
178
 
@@ -36,7 +36,7 @@ const Breadcrumb = ({categories, ...rest}) => {
36
36
  return (
37
37
  <ChakraBreadcrumb
38
38
  className="sf-breadcrumb"
39
- {...styles.container}
39
+ sx={styles.container}
40
40
  separator={<ChevronRightIcon {...styles.icon} aria-hidden="true" />}
41
41
  {...rest}
42
42
  >
@@ -45,7 +45,7 @@ const Breadcrumb = ({categories, ...rest}) => {
45
45
  <ChakraBreadcrumbLink
46
46
  as={RouteLink}
47
47
  to={categoryUrlBuilder(category, intl.locale)}
48
- {...styles.link}
48
+ sx={styles.link}
49
49
  >
50
50
  {category.name}
51
51
  </ChakraBreadcrumbLink>
@@ -60,7 +60,5 @@ test('renders LinksList with horizontal variant', () => {
60
60
  <LinksList links={links} variant="horizontal" />
61
61
  </FooterStylesProvider>
62
62
  )
63
- screen.logTestingPlaygroundURL()
64
-
65
63
  expect(container.querySelector(horizontalVariantSelector)).toBeInTheDocument()
66
64
  })
@@ -63,6 +63,11 @@ beforeEach(() => {
63
63
  )
64
64
  })
65
65
 
66
+ afterEach(() => {
67
+ jest.resetModules()
68
+ jest.restoreAllMocks()
69
+ })
70
+
66
71
  test('renders bundle product view modal', async () => {
67
72
  renderWithProviders(<MockComponent />)
68
73
  await waitFor(() => {
@@ -165,9 +170,9 @@ test('bundle product view modal disables update button when quantity exceeds chi
165
170
  })
166
171
 
167
172
  // Set product bundle quantity selection to 4
168
- quantityInput.focus()
169
173
  fireEvent.change(quantityInput, {target: {value: '4'}})
170
174
  fireEvent.keyDown(quantityInput, {key: 'Enter', code: 'Enter', charCode: 13})
175
+
171
176
  fireEvent.click(sizeSelectBtn)
172
177
 
173
178
  await waitFor(() => {
@@ -11,6 +11,7 @@ import {useIntl} from 'react-intl'
11
11
  import {Button} from '@salesforce/retail-react-app/app/components/shared/ui'
12
12
  import ProductScroller from '@salesforce/retail-react-app/app/components/product-scroller'
13
13
  import useEinstein from '@salesforce/retail-react-app/app/hooks/use-einstein'
14
+ import useDataCloud from '@salesforce/retail-react-app/app/hooks/use-datacloud'
14
15
  import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
15
16
  import useIntersectionObserver from '@salesforce/retail-react-app/app/hooks/use-intersection-observer'
16
17
  import {useWishList} from '@salesforce/retail-react-app/app/hooks/use-wish-list'
@@ -41,6 +42,7 @@ const RecommendedProducts = ({zone, recommender, products, title, shouldFetch, .
41
42
  const {data: customer} = useCurrentCustomer()
42
43
  const {customerId} = customer
43
44
  const {data: wishlist} = useWishList()
45
+ const dataCloud = useDataCloud()
44
46
 
45
47
  const createCustomerProductListItem = useShopperCustomersMutation(
46
48
  'createCustomerProductListItem'
@@ -98,6 +100,13 @@ const RecommendedProducts = ({zone, recommender, products, title, shouldFetch, .
98
100
  },
99
101
  recommendations.recs.map((rec) => ({id: rec.id}))
100
102
  )
103
+ dataCloud.sendViewRecommendations(
104
+ {
105
+ recommenderName: recommendations.recommenderName,
106
+ __recoUUID: recommendations.recoUUID
107
+ },
108
+ recommendations.recs.map((rec) => ({id: rec.id}))
109
+ )
101
110
  }
102
111
  }, [isOnScreen, recommendations])
103
112
 
@@ -354,7 +354,6 @@ describe('StoreLocatorContent', () => {
354
354
  )
355
355
  })
356
356
  )
357
- screen.debug()
358
357
 
359
358
  renderWithProviders(
360
359
  <WrapperComponent