@salesforce/retail-react-app 8.3.0-preview.1 → 8.4.0-nightly-20260116000329
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 +6 -1
- package/app/components/multiship/multiship-order-summary.jsx +6 -2
- package/app/components/order-summary/index.jsx +33 -25
- package/app/constants.js +0 -6
- package/app/hooks/use-password-reset.js +2 -1
- package/app/hooks/use-password-reset.test.js +11 -2
- package/app/pages/account/order-detail.jsx +46 -32
- package/app/pages/account/orders.test.js +116 -0
- package/app/pages/checkout/confirmation.jsx +52 -42
- package/app/pages/login/index.jsx +2 -2
- package/app/pages/login/passwordless-landing.test.js +114 -7
- package/app/pages/reset-password/index.jsx +2 -3
- package/app/pages/reset-password/index.test.jsx +31 -1
- package/app/routes.jsx +34 -28
- package/app/routes.test.js +272 -0
- package/app/utils/bonus-product/cart.js +32 -0
- package/app/utils/bonus-product/cart.test.js +353 -0
- package/app/utils/routes-utils.js +131 -13
- package/app/utils/routes-utils.test.js +177 -0
- package/config/mocks/default.js +5 -1
- package/package.json +6 -6
|
@@ -17,6 +17,11 @@ import Account from '@salesforce/retail-react-app/app/pages/account'
|
|
|
17
17
|
import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
|
|
18
18
|
import {mockedRegisteredCustomer} from '@salesforce/retail-react-app/app/mocks/mock-data'
|
|
19
19
|
import {AuthHelpers} from '@salesforce/commerce-sdk-react'
|
|
20
|
+
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
|
|
21
|
+
|
|
22
|
+
jest.mock('@salesforce/pwa-kit-runtime/utils/ssr-config', () => ({
|
|
23
|
+
getConfig: jest.fn(() => mockConfig)
|
|
24
|
+
}))
|
|
20
25
|
|
|
21
26
|
const mockMergedBasket = {
|
|
22
27
|
basketId: 'a10ff320829cb0eef93ca5310a',
|
|
@@ -45,12 +50,13 @@ const MockedComponent = () => {
|
|
|
45
50
|
)
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
const mockUseRouteMatch = jest.fn(() => ({path: '/'}))
|
|
54
|
+
|
|
48
55
|
jest.mock('react-router', () => {
|
|
56
|
+
const original = jest.requireActual('react-router')
|
|
49
57
|
return {
|
|
50
|
-
...
|
|
51
|
-
useRouteMatch: () =>
|
|
52
|
-
return {path: '/passwordless-login-landing'}
|
|
53
|
-
}
|
|
58
|
+
...original,
|
|
59
|
+
useRouteMatch: () => mockUseRouteMatch()
|
|
54
60
|
}
|
|
55
61
|
})
|
|
56
62
|
|
|
@@ -72,6 +78,14 @@ jest.mock('@salesforce/commerce-sdk-react', () => {
|
|
|
72
78
|
|
|
73
79
|
// Set up and clean up
|
|
74
80
|
beforeEach(() => {
|
|
81
|
+
jest.clearAllMocks()
|
|
82
|
+
getConfig.mockReturnValue(mockConfig)
|
|
83
|
+
|
|
84
|
+
// Reset useRouteMatch mock to return path based on window.location.pathname
|
|
85
|
+
mockUseRouteMatch.mockImplementation(() => ({
|
|
86
|
+
path: typeof window !== 'undefined' && window.location ? window.location.pathname : '/'
|
|
87
|
+
}))
|
|
88
|
+
|
|
75
89
|
global.server.use(
|
|
76
90
|
rest.post('*/customers', (req, res, ctx) => {
|
|
77
91
|
return res(ctx.delay(0), ctx.status(200), ctx.json(mockedRegisteredCustomer))
|
|
@@ -92,11 +106,32 @@ beforeEach(() => {
|
|
|
92
106
|
})
|
|
93
107
|
)
|
|
94
108
|
})
|
|
95
|
-
afterEach(() => {
|
|
96
|
-
jest.resetModules()
|
|
97
|
-
})
|
|
98
109
|
|
|
99
110
|
describe('Passwordless landing tests', function () {
|
|
111
|
+
test('does not run passwordless login when landing path does not match', async () => {
|
|
112
|
+
const token = '11111111'
|
|
113
|
+
const invalidLoginPath = '/invalid-passwordless-login-landing'
|
|
114
|
+
|
|
115
|
+
window.history.pushState(
|
|
116
|
+
{},
|
|
117
|
+
'Passwordless Login Landing',
|
|
118
|
+
createPathWithDefaults(`${invalidLoginPath}?token=${token}`)
|
|
119
|
+
)
|
|
120
|
+
renderWithProviders(<MockedComponent />, {
|
|
121
|
+
wrapperProps: {
|
|
122
|
+
siteAlias: 'uk',
|
|
123
|
+
locale: {id: 'en-GB'},
|
|
124
|
+
appConfig: mockConfig.app
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
await waitFor(() => {
|
|
129
|
+
expect(
|
|
130
|
+
mockAuthHelperFunctions[AuthHelpers.LoginPasswordlessUser].mutateAsync
|
|
131
|
+
).not.toHaveBeenCalled()
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
100
135
|
test('redirects to account page when redirect url is not passed', async () => {
|
|
101
136
|
const token = '12345678'
|
|
102
137
|
window.history.pushState(
|
|
@@ -151,4 +186,76 @@ describe('Passwordless landing tests', function () {
|
|
|
151
186
|
expect(window.location.pathname).toBe('/uk/en-GB/womens-tops')
|
|
152
187
|
})
|
|
153
188
|
})
|
|
189
|
+
|
|
190
|
+
test('detects landing path when at the end of path', async () => {
|
|
191
|
+
const token = '33333333'
|
|
192
|
+
const loginPath = '/global/en-GB/passwordless-login-landing'
|
|
193
|
+
// mockRouteMatch.mockReturnValue({path: loginPath})
|
|
194
|
+
window.history.pushState(
|
|
195
|
+
{},
|
|
196
|
+
'Passwordless Login Landing',
|
|
197
|
+
createPathWithDefaults(`${loginPath}?token=${token}`)
|
|
198
|
+
)
|
|
199
|
+
renderWithProviders(<MockedComponent />, {
|
|
200
|
+
wrapperProps: {
|
|
201
|
+
siteAlias: 'global',
|
|
202
|
+
locale: {id: 'en-GB'},
|
|
203
|
+
appConfig: mockConfig.app
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
await waitFor(() => {
|
|
208
|
+
expect(
|
|
209
|
+
mockAuthHelperFunctions[AuthHelpers.LoginPasswordlessUser].mutateAsync
|
|
210
|
+
).toHaveBeenCalledWith({
|
|
211
|
+
pwdlessLoginToken: token
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
expect(
|
|
216
|
+
mockAuthHelperFunctions[AuthHelpers.LoginPasswordlessUser].mutateAsync
|
|
217
|
+
).toHaveBeenCalledWith({
|
|
218
|
+
pwdlessLoginToken: token
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('landing path changes based on config', async () => {
|
|
223
|
+
const token = '44444444'
|
|
224
|
+
const customLandingPath = '/custom-passwordless-login-landing'
|
|
225
|
+
const mockConfigWithCustomLandingPath = {
|
|
226
|
+
...mockConfig,
|
|
227
|
+
app: {
|
|
228
|
+
...mockConfig.app,
|
|
229
|
+
login: {
|
|
230
|
+
...mockConfig.app.login,
|
|
231
|
+
passwordless: {
|
|
232
|
+
...mockConfig.app.login.passwordless,
|
|
233
|
+
enabled: true,
|
|
234
|
+
landingPath: customLandingPath
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
getConfig.mockReturnValue(mockConfigWithCustomLandingPath)
|
|
241
|
+
|
|
242
|
+
window.history.pushState(
|
|
243
|
+
{},
|
|
244
|
+
'Passwordless Login Landing',
|
|
245
|
+
createPathWithDefaults(`${customLandingPath}?token=${token}`)
|
|
246
|
+
)
|
|
247
|
+
renderWithProviders(<MockedComponent />, {
|
|
248
|
+
wrapperProps: {
|
|
249
|
+
siteAlias: 'uk',
|
|
250
|
+
locale: {id: 'en-GB'},
|
|
251
|
+
appConfig: mockConfigWithCustomLandingPath.app
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
expect(
|
|
256
|
+
mockAuthHelperFunctions[AuthHelpers.LoginPasswordlessUser].mutateAsync
|
|
257
|
+
).toHaveBeenCalledWith({
|
|
258
|
+
pwdlessLoginToken: token
|
|
259
|
+
})
|
|
260
|
+
})
|
|
154
261
|
})
|
|
@@ -20,7 +20,6 @@ import {useLocation} from 'react-router-dom'
|
|
|
20
20
|
import {useRouteMatch} from 'react-router'
|
|
21
21
|
import {usePasswordReset} from '@salesforce/retail-react-app/app/hooks/use-password-reset'
|
|
22
22
|
import {
|
|
23
|
-
RESET_PASSWORD_LANDING_PATH,
|
|
24
23
|
API_ERROR_MESSAGE,
|
|
25
24
|
FEATURE_UNAVAILABLE_ERROR_MESSAGE
|
|
26
25
|
} from '@salesforce/retail-react-app/app/constants'
|
|
@@ -33,7 +32,7 @@ const ResetPassword = () => {
|
|
|
33
32
|
const dataCloud = useDataCloud()
|
|
34
33
|
const {pathname} = useLocation()
|
|
35
34
|
const {path} = useRouteMatch()
|
|
36
|
-
const {getPasswordResetToken} = usePasswordReset()
|
|
35
|
+
const {getPasswordResetToken, resetPasswordLandingPath} = usePasswordReset()
|
|
37
36
|
|
|
38
37
|
const submitForm = async ({email}) => {
|
|
39
38
|
try {
|
|
@@ -71,7 +70,7 @@ const ResetPassword = () => {
|
|
|
71
70
|
marginBottom={8}
|
|
72
71
|
borderRadius="base"
|
|
73
72
|
>
|
|
74
|
-
{path
|
|
73
|
+
{path.endsWith(resetPasswordLandingPath) ? (
|
|
75
74
|
<ResetPasswordLanding />
|
|
76
75
|
) : (
|
|
77
76
|
<ResetPasswordForm
|
|
@@ -14,6 +14,16 @@ import {
|
|
|
14
14
|
import ResetPassword from '.'
|
|
15
15
|
import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
|
|
16
16
|
|
|
17
|
+
const mockUseRouteMatch = jest.fn(() => ({path: '/'}))
|
|
18
|
+
|
|
19
|
+
jest.mock('react-router', () => {
|
|
20
|
+
const original = jest.requireActual('react-router')
|
|
21
|
+
return {
|
|
22
|
+
...original,
|
|
23
|
+
useRouteMatch: () => mockUseRouteMatch()
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
17
27
|
const MockedComponent = () => {
|
|
18
28
|
return (
|
|
19
29
|
<div>
|
|
@@ -24,7 +34,10 @@ const MockedComponent = () => {
|
|
|
24
34
|
|
|
25
35
|
// Set up and clean up
|
|
26
36
|
beforeEach(() => {
|
|
27
|
-
|
|
37
|
+
// Reset useRouteMatch mock to return path based on window.location.pathname
|
|
38
|
+
mockUseRouteMatch.mockImplementation(() => ({
|
|
39
|
+
path: typeof window !== 'undefined' && window.location ? window.location.pathname : '/'
|
|
40
|
+
}))
|
|
28
41
|
window.history.pushState({}, 'Reset Password', createPathWithDefaults('/reset-password'))
|
|
29
42
|
})
|
|
30
43
|
afterEach(() => {
|
|
@@ -75,3 +88,20 @@ test('Allows customer to generate password token', async () => {
|
|
|
75
88
|
expect(window.location.pathname).toBe('/uk/en-GB/login')
|
|
76
89
|
})
|
|
77
90
|
})
|
|
91
|
+
|
|
92
|
+
test.each([
|
|
93
|
+
['base path', '/reset-password-landing'],
|
|
94
|
+
['path with site and locale', '/uk/en-GB/reset-password-landing']
|
|
95
|
+
])('renders reset password landing page when using %s', async (_, landingPath) => {
|
|
96
|
+
window.history.pushState({}, 'Reset Password', createPathWithDefaults(landingPath))
|
|
97
|
+
|
|
98
|
+
// render our test component
|
|
99
|
+
renderWithProviders(<MockedComponent />, {
|
|
100
|
+
wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// check if the landing page is rendered
|
|
104
|
+
await waitFor(() => {
|
|
105
|
+
expect(screen.getByText(/confirm new password/i)).toBeInTheDocument()
|
|
106
|
+
})
|
|
107
|
+
})
|
package/app/routes.jsx
CHANGED
|
@@ -20,14 +20,7 @@ import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
|
|
|
20
20
|
import {Skeleton} from '@salesforce/retail-react-app/app/components/shared/ui'
|
|
21
21
|
import {configureRoutes} from '@salesforce/retail-react-app/app/utils/routes-utils'
|
|
22
22
|
|
|
23
|
-
// Constants
|
|
24
|
-
import {
|
|
25
|
-
PASSWORDLESS_LOGIN_LANDING_PATH,
|
|
26
|
-
RESET_PASSWORD_LANDING_PATH
|
|
27
|
-
} from '@salesforce/retail-react-app/app/constants'
|
|
28
|
-
|
|
29
23
|
const fallback = <Skeleton height="75vh" width="100%" />
|
|
30
|
-
const socialRedirectURI = getConfig()?.app?.login?.social?.redirectURI
|
|
31
24
|
|
|
32
25
|
// Pages
|
|
33
26
|
const Home = loadable(() => import('./pages/home'), {fallback})
|
|
@@ -77,16 +70,6 @@ export const routes = [
|
|
|
77
70
|
component: ResetPassword,
|
|
78
71
|
exact: true
|
|
79
72
|
},
|
|
80
|
-
{
|
|
81
|
-
path: RESET_PASSWORD_LANDING_PATH,
|
|
82
|
-
component: ResetPassword,
|
|
83
|
-
exact: true
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
path: PASSWORDLESS_LOGIN_LANDING_PATH,
|
|
87
|
-
component: Login,
|
|
88
|
-
exact: true
|
|
89
|
-
},
|
|
90
73
|
{
|
|
91
74
|
path: '/account',
|
|
92
75
|
component: Account
|
|
@@ -105,11 +88,6 @@ export const routes = [
|
|
|
105
88
|
component: LoginRedirect,
|
|
106
89
|
exact: true
|
|
107
90
|
},
|
|
108
|
-
{
|
|
109
|
-
path: socialRedirectURI || '/social-callback',
|
|
110
|
-
component: SocialLoginRedirect,
|
|
111
|
-
exact: true
|
|
112
|
-
},
|
|
113
91
|
{
|
|
114
92
|
path: '/cart',
|
|
115
93
|
component: Cart,
|
|
@@ -134,16 +112,44 @@ export const routes = [
|
|
|
134
112
|
{
|
|
135
113
|
path: '/store-locator',
|
|
136
114
|
component: StoreLocator
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
path: '*',
|
|
140
|
-
component: PageNotFound
|
|
141
115
|
}
|
|
142
116
|
]
|
|
143
117
|
|
|
144
118
|
export default () => {
|
|
145
119
|
const config = getConfig()
|
|
146
|
-
|
|
147
|
-
|
|
120
|
+
const loginConfig = config?.app?.login
|
|
121
|
+
const resetPasswordLandingPath = loginConfig?.resetPassword?.landingPath
|
|
122
|
+
const socialLoginEnabled = loginConfig?.social?.enabled
|
|
123
|
+
const socialRedirectURI = loginConfig?.social?.redirectURI
|
|
124
|
+
const passwordlessLoginEnabled = loginConfig?.passwordless?.enabled
|
|
125
|
+
const passwordlessLoginLandingPath = loginConfig?.passwordless?.landingPath
|
|
126
|
+
|
|
127
|
+
// Add dynamic routes conditionally (only if features are enabled and paths are defined)
|
|
128
|
+
const dynamicRoutes = [
|
|
129
|
+
resetPasswordLandingPath && {
|
|
130
|
+
path: resetPasswordLandingPath,
|
|
131
|
+
component: ResetPassword,
|
|
132
|
+
exact: true
|
|
133
|
+
},
|
|
134
|
+
passwordlessLoginEnabled &&
|
|
135
|
+
passwordlessLoginLandingPath && {
|
|
136
|
+
path: passwordlessLoginLandingPath,
|
|
137
|
+
component: Login,
|
|
138
|
+
exact: true
|
|
139
|
+
},
|
|
140
|
+
socialLoginEnabled &&
|
|
141
|
+
socialRedirectURI && {
|
|
142
|
+
path: socialRedirectURI,
|
|
143
|
+
component: SocialLoginRedirect,
|
|
144
|
+
exact: true
|
|
145
|
+
}
|
|
146
|
+
].filter(Boolean)
|
|
147
|
+
|
|
148
|
+
const allRoutes = configureRoutes([...routes, ...dynamicRoutes], config, {
|
|
149
|
+
ignoredRoutes: ['/callback'],
|
|
150
|
+
fuzzyPathMatching: true
|
|
148
151
|
})
|
|
152
|
+
|
|
153
|
+
// Add catch-all route at the end so it doesn't match before dynamic routes
|
|
154
|
+
return [...allRoutes, {path: '*', component: PageNotFound}]
|
|
149
155
|
}
|
package/app/routes.test.js
CHANGED
|
@@ -5,9 +5,281 @@
|
|
|
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 routes from '@salesforce/retail-react-app/app/routes'
|
|
8
|
+
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
|
|
9
|
+
|
|
10
|
+
// Mock getConfig to return test values
|
|
11
|
+
import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
|
|
12
|
+
jest.mock('@salesforce/pwa-kit-runtime/utils/ssr-config', () => ({
|
|
13
|
+
getConfig: jest.fn(() => mockConfig)
|
|
14
|
+
}))
|
|
8
15
|
|
|
9
16
|
describe('Routes', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
jest.clearAllMocks()
|
|
19
|
+
})
|
|
20
|
+
|
|
10
21
|
test('exports a valid react-router configuration', () => {
|
|
11
22
|
expect(Array.isArray(routes) || typeof routes === 'function').toBe(true)
|
|
12
23
|
})
|
|
24
|
+
|
|
25
|
+
test('adds catch-all route at the end', () => {
|
|
26
|
+
const allRoutes = routes()
|
|
27
|
+
const lastRoute = allRoutes.pop()
|
|
28
|
+
expect(lastRoute.path).toBe('*')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('Dynamic routes', () => {
|
|
32
|
+
describe('Reset password landing route', () => {
|
|
33
|
+
test.each([
|
|
34
|
+
['path is null', null],
|
|
35
|
+
['path is empty string', '']
|
|
36
|
+
])('does not add route when %s', (_, landingPath) => {
|
|
37
|
+
getConfig.mockReturnValue({
|
|
38
|
+
...mockConfig,
|
|
39
|
+
app: {
|
|
40
|
+
...mockConfig.app,
|
|
41
|
+
login: {
|
|
42
|
+
resetPassword: {
|
|
43
|
+
landingPath
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const allRoutes = routes()
|
|
50
|
+
const resetPasswordRoute = allRoutes.find(
|
|
51
|
+
(route) => route.path === '/custom-reset-password-landing'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
expect(resetPasswordRoute).toBeUndefined()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('does not add route when landingPath property is missing', () => {
|
|
58
|
+
getConfig.mockReturnValue({
|
|
59
|
+
...mockConfig,
|
|
60
|
+
app: {
|
|
61
|
+
...mockConfig.app,
|
|
62
|
+
login: {
|
|
63
|
+
resetPassword: {}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const allRoutes = routes()
|
|
69
|
+
const resetPasswordRoute = allRoutes.find(
|
|
70
|
+
(route) => route.path === '/reset-password-landing'
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
expect(resetPasswordRoute).toBeUndefined()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('adds route when path is defined', () => {
|
|
77
|
+
getConfig.mockReturnValue({
|
|
78
|
+
...mockConfig,
|
|
79
|
+
app: {
|
|
80
|
+
...mockConfig.app,
|
|
81
|
+
login: {
|
|
82
|
+
resetPassword: {
|
|
83
|
+
landingPath: '/reset-password-landing'
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const allRoutes = routes()
|
|
90
|
+
const resetPasswordRoute = allRoutes.find(
|
|
91
|
+
(route) => route.path === '/reset-password-landing'
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
expect(resetPasswordRoute).toBeDefined()
|
|
95
|
+
expect(resetPasswordRoute.exact).toBe(true)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('Passwordless login landing route', () => {
|
|
100
|
+
test('does not add route when disabled', () => {
|
|
101
|
+
getConfig.mockReturnValue({
|
|
102
|
+
...mockConfig,
|
|
103
|
+
app: {
|
|
104
|
+
...mockConfig.app,
|
|
105
|
+
login: {
|
|
106
|
+
passwordless: {
|
|
107
|
+
enabled: false,
|
|
108
|
+
landingPath: '/custom-passwordless-login-landing'
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const allRoutes = routes()
|
|
115
|
+
const passwordlessRoute = allRoutes.find(
|
|
116
|
+
(route) => route.path === '/custom-passwordless-login-landing'
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
expect(passwordlessRoute).toBeUndefined()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test.each([
|
|
123
|
+
['path is null', null],
|
|
124
|
+
['path is empty string', '']
|
|
125
|
+
])('does not add route when %s', (_, landingPath) => {
|
|
126
|
+
getConfig.mockReturnValue({
|
|
127
|
+
...mockConfig,
|
|
128
|
+
app: {
|
|
129
|
+
...mockConfig.app,
|
|
130
|
+
login: {
|
|
131
|
+
passwordless: {
|
|
132
|
+
enabled: true,
|
|
133
|
+
landingPath
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const allRoutes = routes()
|
|
140
|
+
const passwordlessRoute = allRoutes.find(
|
|
141
|
+
(route) => route.path === '/custom-passwordless-login-landing'
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
expect(passwordlessRoute).toBeUndefined()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('does not add route when landingPath property is missing', () => {
|
|
148
|
+
getConfig.mockReturnValue({
|
|
149
|
+
...mockConfig,
|
|
150
|
+
app: {
|
|
151
|
+
...mockConfig.app,
|
|
152
|
+
login: {
|
|
153
|
+
passwordless: {
|
|
154
|
+
enabled: true
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const allRoutes = routes()
|
|
161
|
+
const passwordlessRoute = allRoutes.find(
|
|
162
|
+
(route) => route.path === '/custom-passwordless-login-landing'
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
expect(passwordlessRoute).toBeUndefined()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('adds route when enabled and path is defined', () => {
|
|
169
|
+
getConfig.mockReturnValue({
|
|
170
|
+
...mockConfig,
|
|
171
|
+
app: {
|
|
172
|
+
...mockConfig.app,
|
|
173
|
+
login: {
|
|
174
|
+
passwordless: {
|
|
175
|
+
enabled: true,
|
|
176
|
+
landingPath: '/passwordless-login-landing'
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const allRoutes = routes()
|
|
183
|
+
const passwordlessRoute = allRoutes.find(
|
|
184
|
+
(route) => route.path === '/passwordless-login-landing'
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
expect(passwordlessRoute).toBeDefined()
|
|
188
|
+
expect(passwordlessRoute.exact).toBe(true)
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe('Social login redirect route', () => {
|
|
193
|
+
test('does not add route when disabled', () => {
|
|
194
|
+
getConfig.mockReturnValue({
|
|
195
|
+
...mockConfig,
|
|
196
|
+
app: {
|
|
197
|
+
...mockConfig.app,
|
|
198
|
+
login: {
|
|
199
|
+
social: {
|
|
200
|
+
enabled: false,
|
|
201
|
+
redirectURI: '/custom-social-callback'
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
const allRoutes = routes()
|
|
208
|
+
const socialRoute = allRoutes.find(
|
|
209
|
+
(route) => route.path === '/custom-social-callback'
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
expect(socialRoute).toBeUndefined()
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test.each([
|
|
216
|
+
['redirectURI is null', null],
|
|
217
|
+
['redirectURI is empty string', '']
|
|
218
|
+
])('does not add route when %s', (_, redirectURI) => {
|
|
219
|
+
getConfig.mockReturnValue({
|
|
220
|
+
...mockConfig,
|
|
221
|
+
app: {
|
|
222
|
+
...mockConfig.app,
|
|
223
|
+
login: {
|
|
224
|
+
social: {
|
|
225
|
+
enabled: true,
|
|
226
|
+
redirectURI
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const allRoutes = routes()
|
|
233
|
+
const socialRoute = allRoutes.find(
|
|
234
|
+
(route) => route.path === '/custom-social-callback'
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
expect(socialRoute).toBeUndefined()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('does not add route when redirectURI property is missing', () => {
|
|
241
|
+
getConfig.mockReturnValue({
|
|
242
|
+
...mockConfig,
|
|
243
|
+
app: {
|
|
244
|
+
...mockConfig.app,
|
|
245
|
+
login: {
|
|
246
|
+
social: {
|
|
247
|
+
enabled: true
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const allRoutes = routes()
|
|
254
|
+
const socialRoute = allRoutes.find(
|
|
255
|
+
(route) => route.path === '/custom-social-callback'
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
expect(socialRoute).toBeUndefined()
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test('adds route when enabled and URI is defined', () => {
|
|
262
|
+
getConfig.mockReturnValue({
|
|
263
|
+
...mockConfig,
|
|
264
|
+
app: {
|
|
265
|
+
...mockConfig.app,
|
|
266
|
+
login: {
|
|
267
|
+
social: {
|
|
268
|
+
enabled: true,
|
|
269
|
+
redirectURI: '/custom-social-callback'
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
const allRoutes = routes()
|
|
276
|
+
const socialRoute = allRoutes.find(
|
|
277
|
+
(route) => route.path === '/custom-social-callback'
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
expect(socialRoute).toBeDefined()
|
|
281
|
+
expect(socialRoute.exact).toBe(true)
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
})
|
|
13
285
|
})
|
|
@@ -553,3 +553,35 @@ export const processProductsForBonusCart = (
|
|
|
553
553
|
|
|
554
554
|
return productItems
|
|
555
555
|
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Consolidates duplicate bonus products in a product items array for display purposes.
|
|
559
|
+
* Groups bonus products by productId and sums their quantities, while keeping non-bonus products as-is.
|
|
560
|
+
*
|
|
561
|
+
* @param {Array<Object>} productItems - Array of product items from basket or order
|
|
562
|
+
* @returns {Array<Object>} Array of product items with consolidated bonus products
|
|
563
|
+
*/
|
|
564
|
+
export const consolidateDuplicateBonusProducts = (productItems) => {
|
|
565
|
+
if (!productItems || productItems.length === 0) {
|
|
566
|
+
return []
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Separate bonus products from regular products
|
|
570
|
+
const bonusProducts = []
|
|
571
|
+
const regularProducts = []
|
|
572
|
+
|
|
573
|
+
productItems.forEach((item) => {
|
|
574
|
+
if (item.bonusProductLineItem) {
|
|
575
|
+
bonusProducts.push(item)
|
|
576
|
+
} else {
|
|
577
|
+
regularProducts.push(item)
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
// Consolidate duplicate bonus products by productId
|
|
582
|
+
const consolidatedBonusProducts = aggregateBonusProductQuantities(bonusProducts)
|
|
583
|
+
|
|
584
|
+
// Combine regular products with consolidated bonus products
|
|
585
|
+
// Keep regular products first, then bonus products
|
|
586
|
+
return [...regularProducts, ...consolidatedBonusProducts]
|
|
587
|
+
}
|