Skip to content

Commit 7ee5e90

Browse files
Merge pull request #3312 from SalesforceCommerceCloud/develop
Update feature branch `feature/manual-bonus-products-v3` with latest changes in `develop` branch
2 parents 6658eae + 4ea115a commit 7ee5e90

File tree

19 files changed

+918
-45
lines changed

19 files changed

+918
-45
lines changed

e2e/scripts/pageHelpers.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ export const searchProduct = async ({page, query, isMobile = false}) => {
439439

440440
let searchInput = isMobile ? searchInputs.nth(1) : searchInputs.nth(0)
441441
await searchInput.fill(query)
442+
await page.waitForTimeout(1000)
442443
await searchInput.press('Enter')
443444

444445
await page.waitForLoadState()

packages/template-retail-react-app/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
## v8.1.0-dev (Sep 04, 2025)
2+
- Updated search UX - prices, images, suggestions new layout [#3271](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3271)
23
- Updated the UI for StoreDisplay component which displays pickup in-store information on different pages. [#3248](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3248)
34
- Added warning modal for guest users when toggling between multi ship and ship to one address. [3280] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3280) [3302] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3302)
45
- Added support for Choice of Bonus Products feature. Users can now select from available bonus products when they qualify for the associated promotion. The bonus product selection flow can be entered from either the "Item Added to Cart" modal (when adding the qualifying product to the cart) or from the cart page. [#3292] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3292)

packages/template-retail-react-app/app/components/search/index.jsx

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import SearchSuggestions from '@salesforce/retail-react-app/app/components/searc
2323
import {SearchIcon} from '@salesforce/retail-react-app/app/components/icons'
2424
import {
2525
capitalize,
26-
boldString,
2726
getSessionJSONItem,
2827
setSessionJSONItem
2928
} from '@salesforce/retail-react-app/app/utils/utils'
@@ -50,15 +49,17 @@ function isAskAgentOnSearchEnabled(enabled, askAgentOnSearch) {
5049
return enabled === 'true' && askAgentOnSearch === 'true' && onClient
5150
}
5251

53-
const formatSuggestions = (searchSuggestions, input) => {
52+
const formatSuggestions = (searchSuggestions) => {
5453
return {
5554
categorySuggestions: searchSuggestions?.categorySuggestions?.categories?.map(
5655
(suggestion) => {
5756
return {
5857
type: 'category',
5958
id: suggestion.id,
6059
link: categoryUrlBuilder({id: suggestion.id}),
61-
name: boldString(suggestion.name, capitalize(input))
60+
name: capitalize(suggestion.name),
61+
image: suggestion.image?.disBaseLink,
62+
parentCategoryName: suggestion.parentCategoryName
6263
}
6364
}
6465
),
@@ -68,19 +69,30 @@ const formatSuggestions = (searchSuggestions, input) => {
6869
currency: product.currency,
6970
price: product.price,
7071
productId: product.productId,
71-
name: boldString(product.productName, capitalize(input)),
72-
link: productUrlBuilder({id: product.productId})
72+
name: capitalize(product.productName),
73+
link: productUrlBuilder({id: product.productId}),
74+
image: product.image?.disBaseLink
7375
}
7476
}),
75-
phraseSuggestions: searchSuggestions?.categorySuggestions?.suggestedPhrases?.map(
77+
brandSuggestions: searchSuggestions?.brandSuggestions?.suggestedPhrases?.map((brand) => {
78+
// Init cap the brand name
79+
return {
80+
type: 'brand',
81+
name: capitalize(brand.phrase),
82+
link: searchUrlBuilder(brand.phrase)
83+
}
84+
}),
85+
phraseSuggestions: searchSuggestions?.productSuggestions?.suggestedPhrases?.map(
7686
(phrase) => {
7787
return {
7888
type: 'phrase',
79-
name: boldString(phrase.phrase, capitalize(input)),
80-
link: searchUrlBuilder(phrase.phrase)
89+
name: phrase.phrase,
90+
link: searchUrlBuilder(phrase.phrase),
91+
exactMatch: phrase.exactMatch
8192
}
8293
}
83-
)
94+
),
95+
searchPhrase: searchSuggestions?.searchPhrase
8496
}
8597
}
8698

@@ -102,10 +114,12 @@ const Search = (props) => {
102114
const [isOpen, setIsOpen] = useState(false)
103115
const [searchQuery, setSearchQuery] = useState('')
104116
const navigate = useNavigation()
117+
105118
const searchSuggestion = useSearchSuggestions(
106119
{
107120
parameters: {
108-
q: searchQuery
121+
q: searchQuery,
122+
expand: 'images,prices'
109123
}
110124
},
111125
{
@@ -119,7 +133,7 @@ const Search = (props) => {
119133
})
120134
const recentSearches = getSessionJSONItem(RECENT_SEARCH_KEY)
121135
const searchSuggestions = useMemo(
122-
() => formatSuggestions(searchSuggestion.data, searchInputRef?.current?.value),
136+
() => formatSuggestions(searchSuggestion.data),
123137
[searchSuggestion]
124138
)
125139

@@ -257,7 +271,7 @@ const Search = (props) => {
257271
// or we have search suggestions available and have inputed some text (empty text in this scenario should show recent searches)
258272
if (
259273
(document.activeElement.id === 'search-input' && recentSearches?.length > 0) ||
260-
(searchSuggestionsAvailable && searchInputRef.current.value.length > 0)
274+
(searchSuggestionsAvailable && searchInputRef.current?.value?.length > 0)
261275
) {
262276
setIsOpen(true)
263277
} else {
@@ -310,7 +324,15 @@ const Search = (props) => {
310324
</PopoverTrigger>
311325

312326
<HideOnMobile>
313-
<PopoverContent data-testid="sf-suggestion-popover">
327+
<PopoverContent
328+
data-testid="sf-suggestion-popover"
329+
width="100vw"
330+
maxWidth="100vw"
331+
left={0}
332+
right={0}
333+
marginLeft={0}
334+
marginRight={0}
335+
>
314336
<SearchSuggestions
315337
closeAndNavigate={closeAndNavigate}
316338
recentSearches={recentSearches}

packages/template-retail-react-app/app/components/search/index.test.js

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,12 @@ test('suggestions render when there are some', async () => {
140140
const suggestionPopoverEl = await screen.getByTestId('sf-suggestion-popover')
141141

142142
await waitFor(() => {
143-
const suggestionsEl = within(suggestionPopoverEl).getByTestId('sf-suggestion')
144-
expect(suggestionsEl.querySelector('button').textContent).toBe('Dresses')
143+
const suggestionsEls = within(suggestionPopoverEl).getAllByTestId('sf-suggestion')
144+
expect(suggestionsEls.length).toBeGreaterThan(0)
145+
const hasDressesSuggestion = suggestionsEls.some((el) =>
146+
el.querySelector('button')?.textContent?.includes('Dresses')
147+
)
148+
expect(hasDressesSuggestion).toBe(true)
145149
})
146150
})
147151

@@ -423,3 +427,96 @@ test('when sendTextMessage and launchChat both fail, no additional send text is
423427
// Verify sendTextMessage was only called once
424428
expect(sendTextMessageSpy).toHaveBeenCalledTimes(1)
425429
})
430+
431+
test('handles search phrase in formatSuggestions', async () => {
432+
const user = setupUserEvent()
433+
434+
const mockResultsWithPhrase = {
435+
...mockSearchResults,
436+
searchPhrase: 'test search phrase'
437+
}
438+
439+
global.server.use(
440+
rest.get('*/search-suggestions', (req, res, ctx) => {
441+
return res(ctx.delay(0), ctx.status(200), ctx.json(mockResultsWithPhrase))
442+
})
443+
)
444+
445+
renderWithProviders(<SearchInput />)
446+
const searchInput = document.querySelector('input[type="search"]')
447+
448+
await user.type(searchInput, 'test')
449+
450+
// Wait for suggestions to load with search phrase
451+
await waitFor(() => {
452+
expect(screen.getByTestId('sf-suggestion-popover')).toBeInTheDocument()
453+
})
454+
})
455+
456+
test('handles phrase suggestions in formatSuggestions', async () => {
457+
const user = setupUserEvent()
458+
459+
// Mock search results with phrase suggestions
460+
const mockResultsWithPhrases = {
461+
...mockSearchResults,
462+
productSuggestions: {
463+
...mockSearchResults.productSuggestions,
464+
suggestedPhrases: [
465+
{phrase: 'running shoes', exactMatch: true},
466+
{phrase: 'athletic wear', exactMatch: false}
467+
]
468+
}
469+
}
470+
471+
global.server.use(
472+
rest.get('*/search-suggestions', (req, res, ctx) => {
473+
return res(ctx.delay(0), ctx.status(200), ctx.json(mockResultsWithPhrases))
474+
})
475+
)
476+
477+
renderWithProviders(<SearchInput />)
478+
const searchInput = document.querySelector('input[type="search"]')
479+
480+
await user.type(searchInput, 'running')
481+
482+
// Wait for suggestions to load with phrase suggestions
483+
await waitFor(() => {
484+
expect(screen.getByTestId('sf-suggestion-popover')).toBeInTheDocument()
485+
})
486+
})
487+
488+
test('handles product suggestions with images', async () => {
489+
const user = setupUserEvent()
490+
491+
// Mock search results with product suggestions that have images
492+
const mockResultsWithProductImages = {
493+
...mockSearchResults,
494+
productSuggestions: {
495+
...mockSearchResults.productSuggestions,
496+
products: [
497+
{
498+
...mockSearchResults.productSuggestions.products[0],
499+
image: {
500+
disBaseLink: 'https://example.com/product-image.jpg'
501+
}
502+
}
503+
]
504+
}
505+
}
506+
507+
global.server.use(
508+
rest.get('*/search-suggestions', (req, res, ctx) => {
509+
return res(ctx.delay(0), ctx.status(200), ctx.json(mockResultsWithProductImages))
510+
})
511+
)
512+
513+
renderWithProviders(<SearchInput />)
514+
const searchInput = document.querySelector('input[type="search"]')
515+
516+
await user.type(searchInput, 'Dress')
517+
518+
// Wait for suggestions to load with product images
519+
await waitFor(() => {
520+
expect(screen.getByTestId('sf-suggestion-popover')).toBeInTheDocument()
521+
})
522+
})
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright (c) 2021, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import React from 'react'
8+
import PropTypes from 'prop-types'
9+
import {Text, Box, Flex, AspectRatio} from '@salesforce/retail-react-app/app/components/shared/ui'
10+
import DynamicImage from '@salesforce/retail-react-app/app/components/dynamic-image'
11+
import Link from '@salesforce/retail-react-app/app/components/link'
12+
import {useStyleConfig} from '@chakra-ui/react'
13+
14+
const HorizontalSuggestions = ({suggestions, closeAndNavigate, dynamicImageProps}) => {
15+
const styles = useStyleConfig('HorizontalSuggestions')
16+
17+
if (!suggestions) {
18+
return null
19+
}
20+
21+
return (
22+
<Box data-testid="sf-horizontal-product-suggestions" sx={styles.container}>
23+
<Flex sx={styles.flexContainer}>
24+
{suggestions.map((suggestion, idx) => (
25+
<Link
26+
data-testid="product-tile"
27+
to={suggestion.link}
28+
key={idx}
29+
onClick={() => closeAndNavigate(suggestion.link)}
30+
>
31+
<Box sx={styles.suggestionItem}>
32+
{/* Product Image */}
33+
<Box sx={styles.imageContainer}>
34+
{suggestion.image ? (
35+
<AspectRatio sx={styles.aspectRatio}>
36+
<DynamicImage
37+
src={`${suggestion.image}[?sw={width}&q=60]`}
38+
widths={dynamicImageProps?.widths}
39+
sx={styles.dynamicImage}
40+
imageProps={{
41+
alt: '',
42+
loading: 'eager'
43+
}}
44+
/>
45+
</AspectRatio>
46+
) : null}
47+
</Box>
48+
49+
<Text sx={styles.productName}>{suggestion.name}</Text>
50+
51+
{suggestion.price && (
52+
<Text sx={styles.productPrice}>${suggestion.price}</Text>
53+
)}
54+
</Box>
55+
</Link>
56+
))}
57+
</Flex>
58+
</Box>
59+
)
60+
}
61+
62+
HorizontalSuggestions.propTypes = {
63+
suggestions: PropTypes.array,
64+
closeAndNavigate: PropTypes.func,
65+
dynamicImageProps: PropTypes.object
66+
}
67+
68+
export default HorizontalSuggestions

0 commit comments

Comments
 (0)