@faststore/core 4.5.0-dev.1 → 4.5.0-dev.3
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/.turbo/turbo-generate.log +3 -3
- package/.turbo/turbo-test.log +71 -73
- package/CHANGELOG.md +10 -0
- package/cms/faststore/components/cms_component__recommendationshelf.jsonc +7 -1
- package/cms/faststore/schema.json +7 -1
- package/discovery.config.default.js +0 -3
- package/package.json +6 -6
- package/src/Layout.tsx +4 -5
- package/src/components/sections/RecommendationShelf/README.md +163 -0
- package/src/components/sections/RecommendationShelf/RecommendationShelf.tsx +3 -4
- package/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts +8 -1
- package/src/sdk/analytics/hooks/useStartRecommendationSession.ts +38 -18
- package/src/sdk/analytics/utils/hasEnabledRecommendationShelf.ts +49 -0
- package/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx +32 -45
- package/test/sdk/analytics/hasEnabledRecommendationShelf.test.ts +88 -0
- package/test/sdk/analytics/useStartRecommendationSession.test.ts +90 -15
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
|
-
> @faststore/core@4.5.0-dev.
|
|
2
|
+
> @faststore/core@4.5.0-dev.2 generate /home/runner/work/faststore/faststore/packages/core
|
|
3
3
|
> pnpm run gen-types && pnpm run cache-graphql
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
> @faststore/core@4.5.0-dev.
|
|
6
|
+
> @faststore/core@4.5.0-dev.2 gen-types /home/runner/work/faststore/faststore/packages/core
|
|
7
7
|
> node ../cli/bin/run generate-types .
|
|
8
8
|
|
|
9
9
|
[33m[STARTED][39m Parse Configuration
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
[32m[COMPLETED][39m Generate to /home/runner/work/faststore/faststore/packages/core/@generated/
|
|
20
20
|
[32m[COMPLETED][39m Generate outputs
|
|
21
21
|
|
|
22
|
-
> @faststore/core@4.5.0-dev.
|
|
22
|
+
> @faststore/core@4.5.0-dev.2 cache-graphql /home/runner/work/faststore/faststore/packages/core
|
|
23
23
|
> node ../cli/bin/run cache-graphql --config=./discovery.config.default.js --queries=./@generated/persisted-documents.json
|
|
24
24
|
|
|
25
25
|
[Info] - Config file location: /home/runner/work/faststore/faststore/packages/core/discovery.config.default.js
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
|
|
2
|
-
> @faststore/core@4.5.0-dev.
|
|
2
|
+
> @faststore/core@4.5.0-dev.2 test /home/runner/work/faststore/faststore/packages/core
|
|
3
3
|
> vitest run
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
[1m[46m RUN [49m[22m [36mv4.0.7 [39m[90m/home/runner/work/faststore/faststore/packages/core[39m
|
|
7
7
|
|
|
8
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/reconcileSessionLocale.browser.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m
|
|
8
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/reconcileSessionLocale.browser.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 42[2mms[22m[39m
|
|
9
9
|
[90mstderr[2m | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx
|
|
10
10
|
[22m[39mError in optimistic validation: TypeError: Failed to parse URL from /api/graphql?operationName=ValidateCartMutation&operationHash=32c15f8888ca34f223def7972b7f19090808435a
|
|
11
11
|
[90m at node:internal/deps/undici/undici:16416:13[39m
|
|
@@ -32,13 +32,12 @@
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/account/ContractSwitcher.browser.test.tsx [2m([22m[2m13 tests[22m[2m)[22m[33m 2028[2mms[22m[39m
|
|
36
|
+
[33m[2m✓[22m[39m shows an error state when the list fails to load [33m 610[2mms[22m[39m
|
|
35
37
|
[90mstderr[2m | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx[2m > [22m[2mRecommendationShelf[2m > [22m[2mrenders nothing when there is an error
|
|
36
38
|
[22m[39mError fetching recommendations [90mundefined[39m boom for campaign vrn:recommendations:acc:rec-top-items-v2:campaign-1
|
|
37
39
|
|
|
38
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx [2m([22m[2m8 tests[22m[2m)[22m[32m
|
|
39
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/account/ContractSwitcher.browser.test.tsx [2m([22m[2m13 tests[22m[2m)[22m[33m 2294[2mms[22m[39m
|
|
40
|
-
[33m[2m✓[22m[39m shows an error state when the list fails to load [33m 682[2mms[22m[39m
|
|
41
|
-
[33m[2m✓[22m[39m requires selecting a contract before Confirm is enabled, then switches (REQ-03) [33m 325[2mms[22m[39m
|
|
40
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx [2m([22m[2m8 tests[22m[2m)[22m[32m 282[2mms[22m[39m
|
|
42
41
|
[90mstderr[2m | test/pages/password-protection.browser.test.tsx[2m > [22m[2mPasswordProtectionLogin[2m > [22m[2mposts the password and redirects to the requested return path
|
|
43
42
|
[22m[39mWarning: An update to PasswordProtectionLogin inside a test was not wrapped in act(...).
|
|
44
43
|
|
|
@@ -268,14 +267,12 @@ act(() => {
|
|
|
268
267
|
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
|
|
269
268
|
at PasswordProtectionLogin (/home/runner/work/faststore/faststore/packages/core/src/pages/password-protection/index.tsx:19:69)
|
|
270
269
|
|
|
271
|
-
[32m✓[39m [30m[46m browser [49m[39m test/pages/password-protection.browser.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[33m
|
|
272
|
-
[33m[2m✓[22m[39m renders the password protection form [33m
|
|
273
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/getInitialSession.browser.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m
|
|
274
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
275
|
-
|
|
276
|
-
[33m[2m✓[22m[39m
|
|
277
|
-
[33m[2m✓[22m[39m opens the contract switcher and returns to the menu on back [33m 529[2mms[22m[39m
|
|
278
|
-
[33m[2m✓[22m[39m resets to the menu when the drawer closes [33m 349[2mms[22m[39m
|
|
270
|
+
[32m✓[39m [30m[46m browser [49m[39m test/pages/password-protection.browser.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[33m 1279[2mms[22m[39m
|
|
271
|
+
[33m[2m✓[22m[39m renders the password protection form [33m 519[2mms[22m[39m
|
|
272
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/getInitialSession.browser.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 27[2mms[22m[39m
|
|
273
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/account/OrganizationDrawer.browser.test.tsx [2m([22m[2m5 tests[22m[2m)[22m[33m 1699[2mms[22m[39m
|
|
274
|
+
[33m[2m✓[22m[39m renders the menu view with contract details and manage footer [33m 978[2mms[22m[39m
|
|
275
|
+
[33m[2m✓[22m[39m opens the contract switcher and returns to the menu on back [33m 349[2mms[22m[39m
|
|
279
276
|
[90mstderr[2m | test/sdk/localization/store-url.browser.test.ts
|
|
280
277
|
[22m[39mError in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
|
|
281
278
|
at validateCart [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/index.ts:119:27[90m)[39m
|
|
@@ -283,9 +280,10 @@ This ensures that you're testing the behavior the user would see in the browser.
|
|
|
283
280
|
at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
|
|
284
281
|
[90m at processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
|
285
282
|
|
|
286
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/
|
|
287
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/
|
|
288
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/
|
|
283
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/changeContractToken.browser.test.ts [2m([22m[2m9 tests[22m[2m)[22m[32m 92[2mms[22m[39m
|
|
284
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/localization/store-url.browser.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 17[2mms[22m[39m
|
|
285
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/useAvailableContracts.browser.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 101[2mms[22m[39m
|
|
286
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/useSwitchContract.browser.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 145[2mms[22m[39m
|
|
289
287
|
[90mstderr[2m | test/components/account/doLogout.browser.test.ts
|
|
290
288
|
[22m[39mError in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
|
|
291
289
|
at validateCart [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/index.ts:119:27[90m)[39m
|
|
@@ -293,7 +291,7 @@ This ensures that you're testing the behavior the user would see in the browser.
|
|
|
293
291
|
at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
|
|
294
292
|
[90m at processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
|
295
293
|
|
|
296
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/account/doLogout.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m
|
|
294
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/account/doLogout.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 87[2mms[22m[39m
|
|
297
295
|
[90mstderr[2m | test/sdk/account/clearPersistedSessionState.browser.test.ts
|
|
298
296
|
[22m[39mError in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
|
|
299
297
|
at validateCart [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/index.ts:119:27[90m)[39m
|
|
@@ -301,10 +299,10 @@ This ensures that you're testing the behavior the user would see in the browser.
|
|
|
301
299
|
at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
|
|
302
300
|
[90m at processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
|
303
301
|
|
|
304
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/account/OrganizationDrawerHeader.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 285[2mms[22m[39m
|
|
305
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/account/OrganizationDrawerFooter.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[33m 317[2mms[22m[39m
|
|
306
302
|
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/clearPersistedSessionState.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 9[2mms[22m[39m
|
|
307
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
303
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/account/OrganizationDrawerFooter.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 286[2mms[22m[39m
|
|
304
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/account/OrganizationDrawerHeader.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 292[2mms[22m[39m
|
|
305
|
+
[32m✓[39m [30m[46m browser [49m[39m test/utils/isLocalHost.browser.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
308
306
|
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/reconcileSessionLocale.disabled.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 3[2mms[22m[39m
|
|
309
307
|
[90mstderr[2m | test/sdk/account/refreshToken.browser.test.ts
|
|
310
308
|
[22m[39mError in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
|
|
@@ -313,12 +311,12 @@ This ensures that you're testing the behavior the user would see in the browser.
|
|
|
313
311
|
at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
|
|
314
312
|
[90m at processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
|
315
313
|
|
|
316
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/localization/bindingPaths.test.ts [2m([22m[2m71 tests[22m[2m)[22m[32m
|
|
317
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/refreshToken.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m
|
|
318
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
319
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
320
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/localization/bindingSelector.test.ts [2m([22m[2m18 tests[22m[2m)[22m[32m
|
|
321
|
-
[32m✓[39m [30m[43m node [49m[39m test/pages/api/unlock.test.ts [2m([22m[2m15 tests[22m[2m)[22m[32m
|
|
314
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/localization/bindingPaths.test.ts [2m([22m[2m71 tests[22m[2m)[22m[32m 25[2mms[22m[39m
|
|
315
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/refreshToken.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 6[2mms[22m[39m
|
|
316
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/password-protection-service.test.ts [2m([22m[2m22 tests[22m[2m)[22m[32m 27[2mms[22m[39m
|
|
317
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/match-url.test.ts [2m([22m[2m10 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
318
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/localization/bindingSelector.test.ts [2m([22m[2m18 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
319
|
+
[32m✓[39m [30m[43m node [49m[39m test/pages/api/unlock.test.ts [2m([22m[2m15 tests[22m[2m)[22m[32m 195[2mms[22m[39m
|
|
322
320
|
[90mstderr[2m | test/components/search/SearchInputComponent.test.tsx
|
|
323
321
|
[22m[39mWarning: forwardRef render functions accept exactly two parameters: props and ref. Did you forget to use the ref parameter?
|
|
324
322
|
|
|
@@ -382,23 +380,23 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
|
|
|
382
380
|
at Suspense
|
|
383
381
|
at Wrapper (/home/runner/work/faststore/faststore/packages/core/test/components/search/SearchInputComponent.test.tsx:106:20)
|
|
384
382
|
|
|
385
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/search/SearchInputComponent.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[32m
|
|
386
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
387
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
388
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/search/useSearchHistory.test.ts [2m([22m[2m13 tests[22m[2m)[22m[32m
|
|
389
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/clearCookies.test.ts [2m([22m[2m20 tests[22m[2m)[22m[32m
|
|
390
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountListQuotesTable.test.tsx [2m([22m[2m12 tests[22m[2m)[22m[33m
|
|
383
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/search/SearchInputComponent.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[32m 67[2mms[22m[39m
|
|
384
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/cookieCacheBusting.test.ts [2m([22m[2m10 tests[22m[2m)[22m[32m 19[2mms[22m[39m
|
|
385
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/localization/useBindingSelector.test.tsx [2m([22m[2m12 tests[22m[2m)[22m[32m 10[2mms[22m[39m
|
|
386
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/search/useSearchHistory.test.ts [2m([22m[2m13 tests[22m[2m)[22m[32m 72[2mms[22m[39m
|
|
387
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/clearCookies.test.ts [2m([22m[2m20 tests[22m[2m)[22m[32m 28[2mms[22m[39m
|
|
388
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountListQuotesTable.test.tsx [2m([22m[2m12 tests[22m[2m)[22m[33m 329[2mms[22m[39m
|
|
391
389
|
[90mstderr[2m | test/sdk/account/useSetPassword.test.ts[2m > [22m[2museSetPassword[2m > [22m[2mmaps wrongcredentials error responses to the existing message
|
|
392
390
|
[22m[39mSet password request failed: Bad Request
|
|
393
391
|
|
|
394
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/useSetPassword.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m
|
|
392
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/useSetPassword.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 39[2mms[22m[39m
|
|
395
393
|
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/quoteFilters.test.ts [2m([22m[2m23 tests[22m[2m)[22m[32m 24[2mms[22m[39m
|
|
396
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountListQuotes.test.tsx [2m([22m[2m9 tests[22m[2m)[22m[33m
|
|
394
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountListQuotes.test.tsx [2m([22m[2m9 tests[22m[2m)[22m[33m 823[2mms[22m[39m
|
|
397
395
|
[33m[2m✓[22m[39m navigates with the search term on blur [33m 359[2mms[22m[39m
|
|
398
|
-
[33m[2m✓[22m[39m navigates without a label param when the search is cleared [33m
|
|
399
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/multipleTemplates.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m
|
|
400
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/localization/localizeRedirectDestination.test.ts [2m([22m[2m11 tests[22m[2m)[22m[32m
|
|
401
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/orderEntry/useOrderEntryOperation.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m
|
|
396
|
+
[33m[2m✓[22m[39m navigates without a label param when the search is cleared [33m 359[2mms[22m[39m
|
|
397
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/multipleTemplates.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 6[2mms[22m[39m
|
|
398
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/localization/localizeRedirectDestination.test.ts [2m([22m[2m11 tests[22m[2m)[22m[32m 8[2mms[22m[39m
|
|
399
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/orderEntry/useOrderEntryOperation.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 38[2mms[22m[39m
|
|
402
400
|
[90mstderr[2m | test/pages/api/graphql.test.ts[2m > [22m[2m/api/graphql error status propagation[2m > [22m[2mpropagates the upstream 400 from a wrapped BadRequestError instead of 500
|
|
403
401
|
[22m[39mGraphql execution returned with error: [
|
|
404
402
|
{
|
|
@@ -445,46 +443,46 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
|
|
|
445
443
|
{ message: [32m'bad'[39m, status: [33m400[39m, type: [32m'BadRequestError'[39m }
|
|
446
444
|
]
|
|
447
445
|
|
|
448
|
-
[32m✓[39m [30m[43m node [49m[39m test/pages/api/graphql.test.ts [2m([22m[2m10 tests[22m[2m)[22m[32m
|
|
449
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/orderEntry/useOrderEntryUpload.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m
|
|
450
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/
|
|
451
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountQuotesFilterSlider.test.tsx [2m([22m[2m5 tests[22m[2m)[22m[32m
|
|
452
|
-
[32m✓[39m [30m[43m node [49m[39m test/proxy.localization.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m
|
|
453
|
-
[32m✓[39m [30m[43m node [49m[39m test/server/index.test.ts [2m([22m[2m7 tests[22m[2m)[22m[33m
|
|
454
|
-
|
|
455
|
-
[32m✓[39m [30m[43m node [49m[39m test/proxy.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m
|
|
456
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
457
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
458
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
446
|
+
[32m✓[39m [30m[43m node [49m[39m test/pages/api/graphql.test.ts [2m([22m[2m10 tests[22m[2m)[22m[32m 17[2mms[22m[39m
|
|
447
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/orderEntry/useOrderEntryUpload.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 56[2mms[22m[39m
|
|
448
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/analytics/useStartRecommendationSession.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 50[2mms[22m[39m
|
|
449
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountQuotesFilterSlider.test.tsx [2m([22m[2m5 tests[22m[2m)[22m[32m 138[2mms[22m[39m
|
|
450
|
+
[32m✓[39m [30m[43m node [49m[39m test/proxy.localization.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 51[2mms[22m[39m
|
|
451
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/index.test.ts [2m([22m[2m7 tests[22m[2m)[22m[33m 699[2mms[22m[39m
|
|
452
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/orderEntry/useOrderEntry.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 30[2mms[22m[39m
|
|
453
|
+
[32m✓[39m [30m[43m node [49m[39m test/proxy.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 32[2mms[22m[39m
|
|
454
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/orderEntry/useOrderFormItems.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 38[2mms[22m[39m
|
|
455
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/webops-api.test.ts [2m([22m[2m9 tests[22m[2m)[22m[32m 21[2mms[22m[39m
|
|
456
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountQuotesSelectedTags.test.tsx [2m([22m[2m8 tests[22m[2m)[22m[32m 105[2mms[22m[39m
|
|
457
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/refreshToken.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 9[2mms[22m[39m
|
|
459
458
|
[32m✓[39m [30m[43m node [49m[39m test/server/cms/global.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
460
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/
|
|
461
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
462
|
-
[32m✓[39m [30m[43m node [49m[39m test/server/content/service.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
459
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/getMyAccountRoutes.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
460
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/content/service.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
463
461
|
[32m✓[39m [30m[43m node [49m[39m test/sdk/localization/store-url.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
464
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/
|
|
465
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
466
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
467
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
462
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/analytics/hasEnabledRecommendationShelf.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
463
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/auth/useAuth.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 20[2mms[22m[39m
|
|
464
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/getRequestHostname.test.ts [2m([22m[2m12 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
465
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountQuotesStatusSelector.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[32m 142[2mms[22m[39m
|
|
466
|
+
[32m✓[39m [30m[43m node [49m[39m test/pages/api/preview.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 8[2mms[22m[39m
|
|
467
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/validateSessionRefreshToken.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 8[2mms[22m[39m
|
|
468
468
|
[32m✓[39m [30m[43m node [49m[39m test/components/search/SearchInput.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
469
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
470
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/analytics/useStartRecommendationSession.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 31[2mms[22m[39m
|
|
471
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/RecommendationShelf/vrn.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 6[2mms[22m[39m
|
|
472
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/RecommendationShelf/useRecommendationUserId.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 193[2mms[22m[39m
|
|
473
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/account/Layout.test.tsx [2m([22m[2m4 tests[22m[2m)[22m[32m 83[2mms[22m[39m
|
|
469
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/RecommendationShelf/vrn.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
474
470
|
[32m✓[39m [30m[43m node [49m[39m test/server/cms/index.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 6[2mms[22m[39m
|
|
475
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/retry.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m
|
|
476
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
471
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/retry.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 13[2mms[22m[39m
|
|
472
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/account/Layout.test.tsx [2m([22m[2m4 tests[22m[2m)[22m[32m 37[2mms[22m[39m
|
|
473
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/RecommendationShelf/useRecommendationUserId.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 190[2mms[22m[39m
|
|
474
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/unlockResponse.test.ts [2m([22m[2m11 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
477
475
|
[32m✓[39m [30m[43m node [49m[39m test/components/RecommendationShelf/useRecommendations.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
478
476
|
[32m✓[39m [30m[43m node [49m[39m test/utils/isLocalHost.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
479
477
|
[32m✓[39m [30m[43m node [49m[39m test/utils/userOrderStatus.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
480
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/account/OrganizationDrawerBody.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m
|
|
481
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/auth/ProfileChallenge.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 43[2mms[22m[39m
|
|
478
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/account/OrganizationDrawerBody.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 37[2mms[22m[39m
|
|
482
479
|
[32m✓[39m [30m[43m node [49m[39m test/sdk/analytics/utils.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
483
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
484
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
[2m
|
|
488
|
-
[2m
|
|
489
|
-
[2m
|
|
480
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/auth/ProfileChallenge.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 30[2mms[22m[39m
|
|
481
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/clearPersistedSessionState.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 9[2mms[22m[39m
|
|
482
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountQuoteStatusBadge.test.tsx [2m([22m[2m2 tests[22m[2m)[22m[32m 23[2mms[22m[39m
|
|
483
|
+
|
|
484
|
+
[2m Test Files [22m [1m[32m71 passed[39m[22m[90m (71)[39m
|
|
485
|
+
[2m Tests [22m [1m[32m548 passed[39m[22m[90m (548)[39m
|
|
486
|
+
[2m Start at [22m 21:46:31
|
|
487
|
+
[2m Duration [22m 36.11s[2m (transform 8.70s, setup 0ms, collect 28.71s, tests 9.95s, environment 48.82s, prepare 1.22s)[22m
|
|
490
488
|
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [4.5.0-dev.3](https://github.com/vtex/faststore/compare/v4.5.0-dev.2...v4.5.0-dev.3) (2026-07-22)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- **core:** move recommendation session start to CMS Global Section ([8589056](https://github.com/vtex/faststore/commit/8589056046b38e5dbc4f8ce6dcc669f7a15a4d85))
|
|
11
|
+
|
|
12
|
+
# [4.5.0-dev.2](https://github.com/vtex/faststore/compare/v4.5.0-dev.1...v4.5.0-dev.2) (2026-07-20)
|
|
13
|
+
|
|
14
|
+
**Note:** Version bump only for package @faststore/core
|
|
15
|
+
|
|
6
16
|
# [4.5.0-dev.1](https://github.com/vtex/faststore/compare/v4.5.0-dev.0...v4.5.0-dev.1) (2026-07-14)
|
|
7
17
|
|
|
8
18
|
### Bug Fixes
|
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
"$componentKey": "RecommendationShelf",
|
|
4
4
|
"$componentTitle": "Recommendation Shelf",
|
|
5
5
|
"title": "Recommendation Shelf",
|
|
6
|
-
"description": "Display a carousel of personalized product recommendations from a VTEX Recommendations campaign.",
|
|
6
|
+
"description": "Display a carousel of personalized product recommendations from a VTEX Recommendations campaign. Enable the shelf with the Enable recommendations toggle (off by default).",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["campaignVrn"],
|
|
9
9
|
"properties": {
|
|
10
|
+
"enableRecommendations": {
|
|
11
|
+
"title": "Enable recommendations?",
|
|
12
|
+
"description": "When enabled, starts the VTEX Recommendations personalization session (once per browser session) and fetches personalized products for this shelf. Off by default so stores that do not use Recommendations never hit the Recommendations API.",
|
|
13
|
+
"type": "boolean",
|
|
14
|
+
"default": false
|
|
15
|
+
},
|
|
10
16
|
"title": {
|
|
11
17
|
"title": "Title",
|
|
12
18
|
"description": "Override the shelf title. If not set, the campaign title will be used.",
|
|
@@ -4434,10 +4434,16 @@
|
|
|
4434
4434
|
"$componentKey": "RecommendationShelf",
|
|
4435
4435
|
"$componentTitle": "Recommendation Shelf",
|
|
4436
4436
|
"title": "Recommendation Shelf",
|
|
4437
|
-
"description": "Display a carousel of personalized product recommendations from a VTEX Recommendations campaign.",
|
|
4437
|
+
"description": "Display a carousel of personalized product recommendations from a VTEX Recommendations campaign. Enable the shelf with the Enable recommendations toggle (off by default).",
|
|
4438
4438
|
"type": "object",
|
|
4439
4439
|
"required": ["campaignVrn"],
|
|
4440
4440
|
"properties": {
|
|
4441
|
+
"enableRecommendations": {
|
|
4442
|
+
"title": "Enable recommendations?",
|
|
4443
|
+
"description": "When enabled, starts the VTEX Recommendations personalization session (once per browser session) and fetches personalized products for this shelf. Off by default so stores that do not use Recommendations never hit the Recommendations API.",
|
|
4444
|
+
"type": "boolean",
|
|
4445
|
+
"default": false
|
|
4446
|
+
},
|
|
4441
4447
|
"title": {
|
|
4442
4448
|
"title": "Title",
|
|
4443
4449
|
"description": "Override the shelf title. If not set, the campaign title will be used.",
|
|
@@ -201,9 +201,6 @@ module.exports = {
|
|
|
201
201
|
},
|
|
202
202
|
refreshToken: false,
|
|
203
203
|
scrollRestoration: false,
|
|
204
|
-
// Opt-in flag for VTEX Recommendations. When disabled (default), the store
|
|
205
|
-
// never starts a personalization session nor calls the Recommendations API.
|
|
206
|
-
enableRecommendations: false,
|
|
207
204
|
/** Package names to transpile (e.g. ['@vtex/components']). Use a non-empty list to enable Next.js transpilation. */
|
|
208
205
|
transpilePackages: [],
|
|
209
206
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faststore/core",
|
|
3
|
-
"version": "4.5.0-dev.
|
|
3
|
+
"version": "4.5.0-dev.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -71,11 +71,11 @@
|
|
|
71
71
|
"style-loader": "^3.3.1",
|
|
72
72
|
"swr": "^2.2.5",
|
|
73
73
|
"use-sync-external-store": "^1.6.0",
|
|
74
|
-
"@faststore/
|
|
75
|
-
"@faststore/
|
|
76
|
-
"@faststore/
|
|
77
|
-
"@faststore/
|
|
78
|
-
"@faststore/
|
|
74
|
+
"@faststore/diagnostics": "4.5.0-dev.3",
|
|
75
|
+
"@faststore/lighthouse": "4.5.0-dev.3",
|
|
76
|
+
"@faststore/sdk": "4.5.0-dev.3",
|
|
77
|
+
"@faststore/api": "4.5.0-dev.3",
|
|
78
|
+
"@faststore/ui": "4.5.0-dev.3"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@cypress/code-coverage": "^3.12.1",
|
package/src/Layout.tsx
CHANGED
|
@@ -6,11 +6,10 @@ import { useStartRecommendationSession } from './sdk/analytics/hooks/useStartRec
|
|
|
6
6
|
function Layout({ children }: PropsWithChildren) {
|
|
7
7
|
const props = isValidElement(children) ? children.props : undefined
|
|
8
8
|
usePageViewEvent(props)
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
useStartRecommendationSession()
|
|
9
|
+
|
|
10
|
+
// Starts the personalization session once per browser session when the page
|
|
11
|
+
// CMS data includes a RecommendationShelf with `enableRecommendations: true`.
|
|
12
|
+
useStartRecommendationSession(props)
|
|
14
13
|
|
|
15
14
|
return <>{children}</>
|
|
16
15
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Recommendation Shelf
|
|
2
|
+
|
|
3
|
+
CMS section that renders a carousel of personalized product recommendations from a **VTEX Recommendations** campaign. It uses the same product cards (`ProductCard`) as other store shelves; items come from a recommendation campaign (cross-sell, similar items, top sellers, personalized, and related types) instead of a search query.
|
|
4
|
+
|
|
5
|
+
- **Data source:** VTEX Recommendations BFF, exposed by `@faststore/api` through the `recommendations` GraphQL query.
|
|
6
|
+
- **Registration:** global CMS component (`$componentKey: "RecommendationShelf"`), available on any page via CMS.
|
|
7
|
+
- **Opt-in via CMS:** enable Recommendations with the **Enable recommendations?** toggle on the shelf (default `false`). No store code changes or feature flags are required.
|
|
8
|
+
|
|
9
|
+
## Contents
|
|
10
|
+
|
|
11
|
+
- [Prerequisites](#prerequisites)
|
|
12
|
+
- [Setup](#setup)
|
|
13
|
+
- [CMS configuration](#cms-configuration)
|
|
14
|
+
- [Campaign types (VRN) and context](#campaign-types-vrn-and-context)
|
|
15
|
+
- [Runtime behavior](#runtime-behavior)
|
|
16
|
+
- [Code-level overrides](#code-level-overrides)
|
|
17
|
+
- [Placement guidelines](#placement-guidelines)
|
|
18
|
+
- [Privacy and cache](#privacy-and-cache)
|
|
19
|
+
- [Troubleshooting](#troubleshooting)
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
The shelf renders products only when the following conditions are met:
|
|
24
|
+
|
|
25
|
+
1. **Enable recommendations on the shelf.** Set **Enable recommendations?** to `true` on the Recommendation Shelf in the CMS. This opts the store into the personalization session (`startRecommendationSession` mutation, owned by Layout) and allows the shelf to fetch recommendations. Without it (default `false`), no session is started and the shelf does not fetch.
|
|
26
|
+
|
|
27
|
+
2. **Active VTEX Recommendations campaign.** Provide a valid campaign **VRN**. Example: `vrn:recommendations:my-account:rec-persona-v2:abc123`.
|
|
28
|
+
|
|
29
|
+
3. **Activity Flow script (optional, recommended).** Used for analytics:
|
|
30
|
+
- reads PDP `product:*` meta tags to capture product view events;
|
|
31
|
+
- reads `data-af-*` attributes on the shelf to capture impression, view, and click events.
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
### 1. Obtain the campaign VRN
|
|
36
|
+
|
|
37
|
+
In the VTEX Recommendations admin, create or select a campaign and copy its **VRN**. The expected format is:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
vrn:recommendations:<account>:<campaign-type>:<campaign-id>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Supported `<campaign-type>` values are listed under [Campaign types (VRN) and context](#campaign-types-vrn-and-context).
|
|
44
|
+
|
|
45
|
+
### 2. Add Recommendation Shelf via CMS
|
|
46
|
+
|
|
47
|
+
Add **Recommendation Shelf** to the target page (home, PDP, PLP, cart, and so on), configure at least **Campaign VRN** (required), and turn **Enable recommendations?** on.
|
|
48
|
+
|
|
49
|
+
## CMS configuration
|
|
50
|
+
|
|
51
|
+
Schema: `cms/faststore/components/cms_component__recommendationshelf.jsonc`.
|
|
52
|
+
|
|
53
|
+
| CMS property | Key | Type | Required | Default | Description |
|
|
54
|
+
| --- | --- | --- | --- | --- | --- |
|
|
55
|
+
| Enable recommendations? | `enableRecommendations` | boolean | No | `false` | Opt-in that starts the personalization session (via Layout) and allows this shelf to fetch recommendations. |
|
|
56
|
+
| Title | `title` | string | No | — | Overrides the shelf title. Falls back to the campaign title. If both are empty, the heading is not rendered. |
|
|
57
|
+
| Campaign VRN | `campaignVrn` | string | **Yes** | — | Recommendation campaign VRN. Validated by regex (see campaign types below). |
|
|
58
|
+
| Items context | `itemsContext` | `PDP` \| `CART` | No | `PDP` | Source of products used as request context. Applies only to context-based campaigns. |
|
|
59
|
+
| **Carousel Configuration** | `carouselConfiguration` | object | No | — | Carousel settings. |
|
|
60
|
+
| › Items per page (desktop) | `itemsPerPageDesktop` | number | No | `4` | Items per page on desktop. |
|
|
61
|
+
| › Items per page (mobile) | `itemsPerPageMobile` | number | No | `2` | Items per page on mobile and tablet (≤ 768px). |
|
|
62
|
+
| › Carousel track variant | `variant` | `slide` \| `scroll` | No | `scroll` | Carousel navigation mode. |
|
|
63
|
+
| › Infinite navigation? | `infiniteMode` | boolean | No | `false` | Infinite navigation (slide variant only). |
|
|
64
|
+
| › Navigation controls | `controls` | `complete` \| `navigationArrows` \| `paginationBullets` | No | `complete` | Visible navigation controls. |
|
|
65
|
+
| **Product Card Configuration** | `productCardConfiguration` | object | No | — | Product card settings. |
|
|
66
|
+
| › Show discount badge? | `showDiscountBadge` | boolean | No | `true` | Shows the discount badge. |
|
|
67
|
+
| › Cards should be bordered? | `bordered` | boolean | No | `true` | Renders bordered cards. |
|
|
68
|
+
|
|
69
|
+
## Campaign types (VRN) and context
|
|
70
|
+
|
|
71
|
+
The campaign type is derived from the `<campaign-type>` segment of the VRN:
|
|
72
|
+
|
|
73
|
+
| Campaign | VRN segment | Internal type | Requires product context? |
|
|
74
|
+
| --- | --- | --- | --- |
|
|
75
|
+
| Cross-Sell | `rec-cross-v2` | `CROSS_SELL` | **Yes** |
|
|
76
|
+
| Similar Items | `rec-similar-v2` | `SIMILAR_ITEMS` | **Yes** |
|
|
77
|
+
| Visual Similarity | `rec-visual-v2` | `VISUAL_SIMILARITY` | **Yes** |
|
|
78
|
+
| Next Interactions | `rec-next-v2` | `NEXT_INTERACTION` | **Yes** |
|
|
79
|
+
| Personalized | `rec-persona-v2` | `PERSONALIZED` | No |
|
|
80
|
+
| Top Sellers | `rec-top-items-v2` | `TOP_ITEMS` | No |
|
|
81
|
+
| Last Seen | `rec-last-v2` | `LAST_SEEN` | No |
|
|
82
|
+
| Search-Based | `rec-search-v2` | `SEARCH_BASED` | No |
|
|
83
|
+
|
|
84
|
+
**Product context (`itemsContext`):**
|
|
85
|
+
|
|
86
|
+
- `PDP` (default): uses the current product detail page product (`productGroupID`). Outside a PDP there is no product context.
|
|
87
|
+
- `CART`: uses the products currently in the cart (deduplicated). Intended for cross-sell on the cart page.
|
|
88
|
+
|
|
89
|
+
Context-based campaigns (cross-sell, similar items, visual similarity, next interaction) skip the request when no context products are available. Context-agnostic campaigns (top sellers, personalized, last seen, search-based) ignore `itemsContext`.
|
|
90
|
+
|
|
91
|
+
## Runtime behavior
|
|
92
|
+
|
|
93
|
+
All recommendation work runs client-side after hydration:
|
|
94
|
+
|
|
95
|
+
1. **Session start (CMS opt-in)** — `Layout` calls `useStartRecommendationSession(pageProps)` once. The hook checks whether any `RecommendationShelf` in the CMS data has `enableRecommendations: true` and no-ops when none do. Multiple enabled shelves on the same page still start the session only once (in-memory lock + session cookie).
|
|
96
|
+
2. **User id resolution** — `useRecommendationUserId()` reads the `vtex-rec-user-id` cookie, retrying until a value is available or the retry budget is exhausted.
|
|
97
|
+
3. **Request arguments** — when enabled, `getRecommendationArguments()` builds `{ userId, campaignVrn, products }` or returns `null` when the VRN is invalid, the user id is missing, or a context-based campaign has no context products. When disabled, arguments stay `null`.
|
|
98
|
+
4. **Fetch** — `useRecommendations()` runs `ClientRecommendationsQuery` and returns `products`, `correlationId`, and `campaign`.
|
|
99
|
+
5. **Render** — shows `ProductShelfSkeleton` while loading; returns `null` on error or empty results; otherwise renders the carousel and optional heading.
|
|
100
|
+
6. **Tracking** — when correlation and campaign identifiers are present, the shelf emits `data-af-*` attributes for Activity Flow. PDP product views use `product:*` meta tags from `pages/[slug]/p.tsx`.
|
|
101
|
+
|
|
102
|
+
Related files:
|
|
103
|
+
|
|
104
|
+
- Shelf: `src/components/sections/RecommendationShelf/`
|
|
105
|
+
- Hook: `src/sdk/analytics/hooks/useStartRecommendationSession.ts`
|
|
106
|
+
- Layout: `src/Layout.tsx`
|
|
107
|
+
|
|
108
|
+
## Code-level overrides
|
|
109
|
+
|
|
110
|
+
In addition to CMS props, the component accepts code-level overrides (not exposed in the CMS schema):
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { RecommendationShelf } from 'src/components/sections/RecommendationShelf'
|
|
114
|
+
|
|
115
|
+
<RecommendationShelf
|
|
116
|
+
enableRecommendations
|
|
117
|
+
campaignVrn="vrn:recommendations:my-account:rec-cross-v2:abc123"
|
|
118
|
+
itemsContext="PDP"
|
|
119
|
+
ProductCard={MyCustomCard}
|
|
120
|
+
mapProductToProductCard={(product, index) => ({
|
|
121
|
+
product,
|
|
122
|
+
index,
|
|
123
|
+
highlight: index === 0,
|
|
124
|
+
})}
|
|
125
|
+
carouselConfiguration={{ itemsPerPageDesktop: 5, variant: 'slide' }}
|
|
126
|
+
/>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- **`ProductCard`**: custom card component (defaults to the core `ProductCard`).
|
|
130
|
+
- **`mapProductToProductCard`**: maps each recommended product (normalized `StoreProduct`) into card props. When provided, it fully owns the card props and the default `productCardConfiguration` merge no longer applies.
|
|
131
|
+
|
|
132
|
+
Campaign types and VRN validation are defined in `vrn.ts` (`VRN_TYPE_TO_RECOMMENDATION`). Updates should be made there and mirrored in the CMS schema `pattern`.
|
|
133
|
+
|
|
134
|
+
## Placement guidelines
|
|
135
|
+
|
|
136
|
+
As a global component, the shelf can be placed on any CMS page. Suggested placements by campaign type:
|
|
137
|
+
|
|
138
|
+
- **Home / institutional pages:** context-agnostic campaigns (top sellers, personalized, last seen, search-based).
|
|
139
|
+
- **PDP:** context-based campaigns with `itemsContext: PDP`.
|
|
140
|
+
- **Cart:** cross-sell with `itemsContext: CART`.
|
|
141
|
+
- **PLP:** context-agnostic campaigns.
|
|
142
|
+
|
|
143
|
+
The personalization session runs only when at least one shelf has **Enable recommendations?** turned on.
|
|
144
|
+
|
|
145
|
+
## Privacy and cache
|
|
146
|
+
|
|
147
|
+
- The `recommendations` query uses `@cacheControl(scope: "private", sMaxAge: 120, staleWhileRevalidate: 3600)`.
|
|
148
|
+
- Error logs omit `recommendationArgs` (which includes `userId`) and log only non-identifying context such as `campaignVrn`.
|
|
149
|
+
- BFF `vtex-rec-*` cookies are forwarded to the browser through `ctx.storage.cookies`.
|
|
150
|
+
|
|
151
|
+
## Troubleshooting
|
|
152
|
+
|
|
153
|
+
**The shelf does not render.** Check, in order:
|
|
154
|
+
|
|
155
|
+
1. Is **Enable recommendations?** set to `true` on the shelf?
|
|
156
|
+
2. Is `campaignVrn` valid and is the campaign active?
|
|
157
|
+
3. Is the `vtex-rec-user-id` cookie available? Without a user id, the shelf does not fetch.
|
|
158
|
+
4. For context-based campaigns, is product context available (`PDP` on a product page, or a non-empty cart for `CART`)?
|
|
159
|
+
5. Did the campaign return products? An empty response renders nothing by design.
|
|
160
|
+
|
|
161
|
+
**Skeleton never resolves.** Inspect the console and the GraphQL `recommendations` response for pending or failing requests.
|
|
162
|
+
|
|
163
|
+
**No tracking events.** Confirm that the Activity Flow script is present and that PDP `product:*` meta tags and shelf `data-af-*` attributes are rendered.
|
|
@@ -4,8 +4,6 @@ import { usePDP } from '@faststore/core'
|
|
|
4
4
|
import { ProductShelf, Carousel } from '@faststore/ui'
|
|
5
5
|
import type { ProductSummary_ProductFragment } from '@generated/graphql'
|
|
6
6
|
|
|
7
|
-
import storeConfig from 'discovery.config'
|
|
8
|
-
|
|
9
7
|
import DefaultProductCard, {
|
|
10
8
|
type ProductCardProps,
|
|
11
9
|
} from 'src/components/product/ProductCard'
|
|
@@ -66,6 +64,7 @@ export function RecommendationShelf<
|
|
|
66
64
|
>({
|
|
67
65
|
title,
|
|
68
66
|
campaignVrn,
|
|
67
|
+
enableRecommendations = false,
|
|
69
68
|
itemsContext = 'PDP',
|
|
70
69
|
ProductCard,
|
|
71
70
|
mapProductToProductCard,
|
|
@@ -120,9 +119,9 @@ export function RecommendationShelf<
|
|
|
120
119
|
return pdpProduct ? [pdpProduct] : []
|
|
121
120
|
}, [itemsContext, cartItems, productDetailPage])
|
|
122
121
|
|
|
123
|
-
// Gate the whole feature behind the opt-in
|
|
122
|
+
// Gate the whole feature behind the CMS opt-in: when Recommendations is
|
|
124
123
|
// disabled the shelf never requests recommendations (nor renders).
|
|
125
|
-
const recommendationArgs =
|
|
124
|
+
const recommendationArgs = enableRecommendations
|
|
126
125
|
? getRecommendationArguments(campaignVrn, {
|
|
127
126
|
userId,
|
|
128
127
|
contextProducts,
|
|
@@ -30,6 +30,13 @@ export type RecommendationShelfProps<
|
|
|
30
30
|
> = {
|
|
31
31
|
readonly title?: string
|
|
32
32
|
readonly campaignVrn: string
|
|
33
|
+
/**
|
|
34
|
+
* Opt-in for VTEX Recommendations on this shelf. When `true`, Layout starts
|
|
35
|
+
* the personalization session (`useStartRecommendationSession`) and this
|
|
36
|
+
* shelf fetches recommendations. When `false` (default), neither happens.
|
|
37
|
+
* @default false
|
|
38
|
+
*/
|
|
39
|
+
readonly enableRecommendations?: boolean
|
|
33
40
|
/**
|
|
34
41
|
* Where to read the products used as context for the recommendation request:
|
|
35
42
|
* - `'PDP'`: the current product detail page product.
|
|
@@ -45,7 +52,7 @@ export type RecommendationShelfProps<
|
|
|
45
52
|
/**
|
|
46
53
|
* Custom card component rendered for each recommended product. Defaults to the
|
|
47
54
|
* core `ProductCard`. This is a code-level override and is not exposed through
|
|
48
|
-
* the CMS schema (`
|
|
55
|
+
* the CMS schema (`cms_component__recommendationshelf.jsonc`).
|
|
49
56
|
*/
|
|
50
57
|
readonly ProductCard?: ComponentType<TCardProps>
|
|
51
58
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
2
|
|
|
3
3
|
import { gql } from '@faststore/core/api'
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { hasEnabledRecommendationShelf } from 'src/sdk/analytics/utils/hasEnabledRecommendationShelf'
|
|
6
6
|
import { useLazyQuery } from 'src/sdk/graphql/useLazyQuery'
|
|
7
7
|
import { getCookie } from 'src/utils/getCookie'
|
|
8
8
|
import { retry } from 'src/utils/retry'
|
|
@@ -20,12 +20,28 @@ const startRecommendationSessionMutation = gql(`
|
|
|
20
20
|
type StartRecommendationSessionData = { startRecommendationSession: boolean }
|
|
21
21
|
type StartRecommendationSessionVariables = Record<string, never>
|
|
22
22
|
|
|
23
|
+
// In-memory lock so Layout re-renders (e.g. multiple RecommendationShelves
|
|
24
|
+
// fetching) cannot start more than one session before the cookie lands.
|
|
25
|
+
// Survives effect cleanup / Strict Mode remounts for this JS realm.
|
|
26
|
+
let hasRequestedStartRecommendationSession = false
|
|
27
|
+
|
|
28
|
+
/** @internal Test-only: clears the in-memory session-start lock. */
|
|
29
|
+
export function resetStartRecommendationSessionLock() {
|
|
30
|
+
hasRequestedStartRecommendationSession = false
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
/**
|
|
24
34
|
* Starts the anonymous recommendation/personalization session once per browser
|
|
25
35
|
* session.
|
|
26
36
|
*
|
|
27
|
-
* Mounted
|
|
28
|
-
*
|
|
37
|
+
* Mounted from `Layout` with the current page props. Opt-in is controlled by
|
|
38
|
+
* the CMS boolean prop `enableRecommendations` on `RecommendationShelf`
|
|
39
|
+
* (default `false`): if no shelf on the page has it enabled, this hook is a
|
|
40
|
+
* no-op and the store never hits the Recommendations API.
|
|
41
|
+
*
|
|
42
|
+
* Multiple enabled shelves on the same page still produce a single call: the
|
|
43
|
+
* hook runs once from Layout and is gated by an in-memory lock plus the
|
|
44
|
+
* session cookie.
|
|
29
45
|
*
|
|
30
46
|
* The product view is no longer reported through a mutation here: the PDP now
|
|
31
47
|
* exposes the product data as `<meta property="product:*">` tags, which the
|
|
@@ -33,11 +49,10 @@ type StartRecommendationSessionVariables = Record<string, never>
|
|
|
33
49
|
*
|
|
34
50
|
* All work runs client-side in effects (after hydration), so it doesn't affect
|
|
35
51
|
* SSR/TTFB or Lighthouse render metrics.
|
|
36
|
-
*
|
|
37
|
-
* Gated behind the `experimental.enableRecommendations` flag: stores that don't
|
|
38
|
-
* opt into Recommendations never start a session nor hit the Recommendations API.
|
|
39
52
|
*/
|
|
40
|
-
export function useStartRecommendationSession() {
|
|
53
|
+
export function useStartRecommendationSession(pageProps?: unknown) {
|
|
54
|
+
const enableRecommendations = hasEnabledRecommendationShelf(pageProps)
|
|
55
|
+
|
|
41
56
|
const [runStartRecommendationSession] = useLazyQuery<
|
|
42
57
|
StartRecommendationSessionData,
|
|
43
58
|
StartRecommendationSessionVariables
|
|
@@ -49,30 +64,40 @@ export function useStartRecommendationSession() {
|
|
|
49
64
|
}
|
|
50
65
|
)
|
|
51
66
|
|
|
67
|
+
// useLazyQuery returns a new `execute` identity every render; keep a stable
|
|
68
|
+
// ref so the effect does not re-subscribe (and re-fire) on unrelated updates.
|
|
69
|
+
const runStartRecommendationSessionRef = useRef(runStartRecommendationSession)
|
|
70
|
+
runStartRecommendationSessionRef.current = runStartRecommendationSession
|
|
71
|
+
|
|
52
72
|
useEffect(() => {
|
|
53
|
-
if (!
|
|
73
|
+
if (!enableRecommendations) {
|
|
54
74
|
return
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
const startRecommendationSessionCookie = getCookie(
|
|
58
78
|
VTEX_REC_USER_START_SESSION
|
|
59
79
|
)
|
|
60
|
-
|
|
61
80
|
if (startRecommendationSessionCookie) {
|
|
62
81
|
return
|
|
63
82
|
}
|
|
64
83
|
|
|
65
|
-
|
|
84
|
+
if (hasRequestedStartRecommendationSession) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
hasRequestedStartRecommendationSession = true
|
|
66
88
|
|
|
67
89
|
// The session endpoint may not be ready on the first try, so we retry with
|
|
68
90
|
// exponential backoff until it returns a defined result (or we give up).
|
|
69
91
|
// Transient request/GraphQL rejections are treated as retryable: swallow
|
|
70
92
|
// the error and return `undefined` so the backoff loop keeps going instead
|
|
71
93
|
// of aborting on the first failure.
|
|
94
|
+
//
|
|
95
|
+
// Intentionally no AbortController: cleanup would cancel the only attempt
|
|
96
|
+
// (Strict Mode remount / Layout re-render) while the lock stays set.
|
|
72
97
|
void retry(
|
|
73
98
|
async () => {
|
|
74
99
|
try {
|
|
75
|
-
return await
|
|
100
|
+
return await runStartRecommendationSessionRef.current({})
|
|
76
101
|
} catch {
|
|
77
102
|
return undefined
|
|
78
103
|
}
|
|
@@ -83,14 +108,9 @@ export function useStartRecommendationSession() {
|
|
|
83
108
|
backoff: true,
|
|
84
109
|
maxDelayMs: 3000,
|
|
85
110
|
until: (result) => result !== undefined,
|
|
86
|
-
signal: controller.signal,
|
|
87
111
|
}
|
|
88
112
|
// Guard the discarded promise so a give-up (or any unexpected rejection)
|
|
89
113
|
// never surfaces as an unhandled promise rejection.
|
|
90
114
|
).catch(() => {})
|
|
91
|
-
|
|
92
|
-
return () => {
|
|
93
|
-
controller.abort()
|
|
94
|
-
}
|
|
95
|
-
}, [runStartRecommendationSession])
|
|
115
|
+
}, [enableRecommendations])
|
|
96
116
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
type CmsSection = {
|
|
2
|
+
name?: string
|
|
3
|
+
$componentKey?: string
|
|
4
|
+
data?: {
|
|
5
|
+
enableRecommendations?: boolean
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function pushSections(target: CmsSection[], value: unknown) {
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
for (const item of value) {
|
|
12
|
+
if (item && typeof item === 'object') {
|
|
13
|
+
target.push(item as CmsSection)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (value && typeof value === 'object') {
|
|
20
|
+
const sections = (value as { sections?: unknown }).sections
|
|
21
|
+
if (Array.isArray(sections)) {
|
|
22
|
+
pushSections(target, sections)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns true when the current page CMS data includes at least one
|
|
29
|
+
* `RecommendationShelf` with `enableRecommendations: true`.
|
|
30
|
+
*/
|
|
31
|
+
export function hasEnabledRecommendationShelf(pageProps: unknown): boolean {
|
|
32
|
+
if (!pageProps || typeof pageProps !== 'object') {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const props = pageProps as Record<string, unknown>
|
|
37
|
+
const sections: CmsSection[] = []
|
|
38
|
+
|
|
39
|
+
pushSections(sections, props.sections)
|
|
40
|
+
pushSections(sections, props.globalSections)
|
|
41
|
+
pushSections(sections, props.page)
|
|
42
|
+
|
|
43
|
+
return sections.some(
|
|
44
|
+
(section) =>
|
|
45
|
+
(section.name === 'RecommendationShelf' ||
|
|
46
|
+
section.$componentKey === 'RecommendationShelf') &&
|
|
47
|
+
section.data?.enableRecommendations === true
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -47,27 +47,6 @@ vi.mock(
|
|
|
47
47
|
})
|
|
48
48
|
)
|
|
49
49
|
|
|
50
|
-
// Keep the real store config (the cart SDK reads `storeConfig.cart` at module
|
|
51
|
-
// init) and only override the opt-in flag, which we toggle per test.
|
|
52
|
-
const recommendationsFlag = vi.hoisted(() => ({ enabled: true }))
|
|
53
|
-
vi.mock('discovery.config', async (original) => {
|
|
54
|
-
const actual = (await original()) as { default?: Record<string, unknown> }
|
|
55
|
-
const base = (actual.default ?? actual) as Record<string, unknown>
|
|
56
|
-
const experimental = (base.experimental ?? {}) as Record<string, unknown>
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
default: {
|
|
60
|
-
...base,
|
|
61
|
-
experimental: {
|
|
62
|
-
...experimental,
|
|
63
|
-
get enableRecommendations() {
|
|
64
|
-
return recommendationsFlag.enabled
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
|
|
71
50
|
import { RecommendationShelf } from 'src/components/sections/RecommendationShelf/RecommendationShelf'
|
|
72
51
|
|
|
73
52
|
const CAMPAIGN_VRN = 'vrn:recommendations:acc:rec-top-items-v2:campaign-1'
|
|
@@ -91,7 +70,6 @@ beforeEach(() => {
|
|
|
91
70
|
loading: false,
|
|
92
71
|
})
|
|
93
72
|
useRecommendationUserId.mockReturnValue('user-1')
|
|
94
|
-
recommendationsFlag.enabled = true
|
|
95
73
|
})
|
|
96
74
|
|
|
97
75
|
afterEach(() => {
|
|
@@ -99,7 +77,7 @@ afterEach(() => {
|
|
|
99
77
|
})
|
|
100
78
|
|
|
101
79
|
describe('RecommendationShelf', () => {
|
|
102
|
-
it('renders a product card per recommended product', async () => {
|
|
80
|
+
it('renders a product card per recommended product when enabled', async () => {
|
|
103
81
|
useRecommendations.mockReturnValue({
|
|
104
82
|
data: recommendationData,
|
|
105
83
|
isLoading: false,
|
|
@@ -107,7 +85,7 @@ describe('RecommendationShelf', () => {
|
|
|
107
85
|
})
|
|
108
86
|
|
|
109
87
|
const { getAllByTestId, getByText } = render(
|
|
110
|
-
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} />
|
|
88
|
+
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} enableRecommendations />
|
|
111
89
|
)
|
|
112
90
|
|
|
113
91
|
await waitFor(() => {
|
|
@@ -126,7 +104,9 @@ describe('RecommendationShelf', () => {
|
|
|
126
104
|
error: null,
|
|
127
105
|
})
|
|
128
106
|
|
|
129
|
-
render(
|
|
107
|
+
render(
|
|
108
|
+
<RecommendationShelf campaignVrn={CROSS_SELL_VRN} enableRecommendations />
|
|
109
|
+
)
|
|
130
110
|
|
|
131
111
|
await waitFor(() => {
|
|
132
112
|
const lastArgs = useRecommendations.mock.calls.at(-1)?.[0]
|
|
@@ -146,7 +126,11 @@ describe('RecommendationShelf', () => {
|
|
|
146
126
|
})
|
|
147
127
|
|
|
148
128
|
const { getByText } = render(
|
|
149
|
-
<RecommendationShelf
|
|
129
|
+
<RecommendationShelf
|
|
130
|
+
campaignVrn={CAMPAIGN_VRN}
|
|
131
|
+
enableRecommendations
|
|
132
|
+
title="Custom title"
|
|
133
|
+
/>
|
|
150
134
|
)
|
|
151
135
|
|
|
152
136
|
await waitFor(() => {
|
|
@@ -162,6 +146,25 @@ describe('RecommendationShelf', () => {
|
|
|
162
146
|
error: null,
|
|
163
147
|
})
|
|
164
148
|
|
|
149
|
+
const { container, queryByTestId } = render(
|
|
150
|
+
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} enableRecommendations />
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
await waitFor(() => {
|
|
154
|
+
const lastArgs = useRecommendations.mock.calls.at(-1)?.[0]
|
|
155
|
+
expect(lastArgs).toBeNull()
|
|
156
|
+
})
|
|
157
|
+
expect(queryByTestId('product-card')).toBeNull()
|
|
158
|
+
expect(container).toBeEmptyDOMElement()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('skips the fetch when recommendations are disabled (default)', async () => {
|
|
162
|
+
useRecommendations.mockReturnValue({
|
|
163
|
+
data: undefined,
|
|
164
|
+
isLoading: false,
|
|
165
|
+
error: null,
|
|
166
|
+
})
|
|
167
|
+
|
|
165
168
|
const { container, queryByTestId } = render(
|
|
166
169
|
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} />
|
|
167
170
|
)
|
|
@@ -182,7 +185,7 @@ describe('RecommendationShelf', () => {
|
|
|
182
185
|
})
|
|
183
186
|
|
|
184
187
|
const { container } = render(
|
|
185
|
-
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} />
|
|
188
|
+
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} enableRecommendations />
|
|
186
189
|
)
|
|
187
190
|
|
|
188
191
|
expect(container).toBeEmptyDOMElement()
|
|
@@ -196,7 +199,7 @@ describe('RecommendationShelf', () => {
|
|
|
196
199
|
})
|
|
197
200
|
|
|
198
201
|
const { container, queryByTestId } = render(
|
|
199
|
-
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} />
|
|
202
|
+
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} enableRecommendations />
|
|
200
203
|
)
|
|
201
204
|
|
|
202
205
|
expect(queryByTestId('product-card')).toBeNull()
|
|
@@ -211,25 +214,9 @@ describe('RecommendationShelf', () => {
|
|
|
211
214
|
})
|
|
212
215
|
|
|
213
216
|
const { getByTestId } = render(
|
|
214
|
-
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} />
|
|
217
|
+
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} enableRecommendations />
|
|
215
218
|
)
|
|
216
219
|
|
|
217
220
|
expect(getByTestId('skeleton').getAttribute('data-loading')).toBe('true')
|
|
218
221
|
})
|
|
219
|
-
|
|
220
|
-
it('skips the fetch when recommendations are disabled', async () => {
|
|
221
|
-
recommendationsFlag.enabled = false
|
|
222
|
-
useRecommendations.mockReturnValue({
|
|
223
|
-
data: undefined,
|
|
224
|
-
isLoading: false,
|
|
225
|
-
error: null,
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
render(<RecommendationShelf campaignVrn={CROSS_SELL_VRN} />)
|
|
229
|
-
|
|
230
|
-
await waitFor(() => {
|
|
231
|
-
const lastArgs = useRecommendations.mock.calls.at(-1)?.[0]
|
|
232
|
-
expect(lastArgs).toBeNull()
|
|
233
|
-
})
|
|
234
|
-
})
|
|
235
222
|
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { hasEnabledRecommendationShelf } from 'src/sdk/analytics/utils/hasEnabledRecommendationShelf'
|
|
4
|
+
|
|
5
|
+
describe('hasEnabledRecommendationShelf', () => {
|
|
6
|
+
it('returns false for empty or unrelated page props', () => {
|
|
7
|
+
expect(hasEnabledRecommendationShelf(undefined)).toBe(false)
|
|
8
|
+
expect(hasEnabledRecommendationShelf({})).toBe(false)
|
|
9
|
+
expect(
|
|
10
|
+
hasEnabledRecommendationShelf({
|
|
11
|
+
sections: [{ name: 'ProductShelf', data: {} }],
|
|
12
|
+
})
|
|
13
|
+
).toBe(false)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('returns true when a page section enables recommendations', () => {
|
|
17
|
+
expect(
|
|
18
|
+
hasEnabledRecommendationShelf({
|
|
19
|
+
sections: [
|
|
20
|
+
{
|
|
21
|
+
name: 'RecommendationShelf',
|
|
22
|
+
data: { enableRecommendations: true, campaignVrn: 'vrn:x' },
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
})
|
|
26
|
+
).toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns true when a global section enables recommendations', () => {
|
|
30
|
+
expect(
|
|
31
|
+
hasEnabledRecommendationShelf({
|
|
32
|
+
globalSections: {
|
|
33
|
+
sections: [
|
|
34
|
+
{
|
|
35
|
+
$componentKey: 'RecommendationShelf',
|
|
36
|
+
data: { enableRecommendations: true },
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('returns true when page.sections enables recommendations', () => {
|
|
45
|
+
expect(
|
|
46
|
+
hasEnabledRecommendationShelf({
|
|
47
|
+
page: {
|
|
48
|
+
sections: [
|
|
49
|
+
{
|
|
50
|
+
name: 'RecommendationShelf',
|
|
51
|
+
data: { enableRecommendations: true },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('returns false when the shelf is present but enableRecommendations is false', () => {
|
|
60
|
+
expect(
|
|
61
|
+
hasEnabledRecommendationShelf({
|
|
62
|
+
sections: [
|
|
63
|
+
{
|
|
64
|
+
name: 'RecommendationShelf',
|
|
65
|
+
data: { enableRecommendations: false },
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
})
|
|
69
|
+
).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('returns true when at least one of multiple shelves enables recommendations', () => {
|
|
73
|
+
expect(
|
|
74
|
+
hasEnabledRecommendationShelf({
|
|
75
|
+
sections: [
|
|
76
|
+
{
|
|
77
|
+
name: 'RecommendationShelf',
|
|
78
|
+
data: { enableRecommendations: false },
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'RecommendationShelf',
|
|
82
|
+
data: { enableRecommendations: true },
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
})
|
|
86
|
+
).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -18,17 +18,50 @@ vi.mock('src/sdk/graphql/useLazyQuery', () => ({
|
|
|
18
18
|
const getCookie = vi.hoisted(() => vi.fn())
|
|
19
19
|
vi.mock('src/utils/getCookie', () => ({ getCookie }))
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
import {
|
|
22
|
+
resetStartRecommendationSessionLock,
|
|
23
|
+
useStartRecommendationSession,
|
|
24
|
+
} from 'src/sdk/analytics/hooks/useStartRecommendationSession'
|
|
25
|
+
|
|
26
|
+
const enabledPageProps = {
|
|
27
|
+
sections: [
|
|
28
|
+
{
|
|
29
|
+
name: 'RecommendationShelf',
|
|
30
|
+
data: { enableRecommendations: true },
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const multipleEnabledShelvesPageProps = {
|
|
36
|
+
sections: [
|
|
37
|
+
{
|
|
38
|
+
name: 'RecommendationShelf',
|
|
39
|
+
data: { enableRecommendations: true, campaignVrn: 'vrn:a' },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'RecommendationShelf',
|
|
43
|
+
data: { enableRecommendations: true, campaignVrn: 'vrn:b' },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'ProductShelf',
|
|
47
|
+
data: {},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const disabledPageProps = {
|
|
53
|
+
sections: [
|
|
54
|
+
{
|
|
55
|
+
name: 'RecommendationShelf',
|
|
56
|
+
data: { enableRecommendations: false },
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
}
|
|
27
60
|
|
|
28
61
|
beforeEach(() => {
|
|
62
|
+
resetStartRecommendationSessionLock()
|
|
29
63
|
useLazyQueryMock.mockReturnValue([runStartRecommendationSession, {}])
|
|
30
64
|
runStartRecommendationSession.mockResolvedValue(true)
|
|
31
|
-
storeConfigMock.experimental.enableRecommendations = true
|
|
32
65
|
})
|
|
33
66
|
|
|
34
67
|
afterEach(() => {
|
|
@@ -36,34 +69,76 @@ afterEach(() => {
|
|
|
36
69
|
})
|
|
37
70
|
|
|
38
71
|
describe('useStartRecommendationSession', () => {
|
|
72
|
+
it('does not start a session when no RecommendationShelf is enabled in CMS', async () => {
|
|
73
|
+
getCookie.mockReturnValue(undefined)
|
|
74
|
+
|
|
75
|
+
renderHook(() => useStartRecommendationSession(disabledPageProps))
|
|
76
|
+
|
|
77
|
+
await waitFor(() => {
|
|
78
|
+
expect(runStartRecommendationSession).not.toHaveBeenCalled()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('does not start a session when page props are missing', async () => {
|
|
83
|
+
getCookie.mockReturnValue(undefined)
|
|
84
|
+
|
|
85
|
+
renderHook(() => useStartRecommendationSession())
|
|
86
|
+
|
|
87
|
+
await waitFor(() => {
|
|
88
|
+
expect(runStartRecommendationSession).not.toHaveBeenCalled()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
39
92
|
it('does not start a session when the session cookie already exists', async () => {
|
|
40
93
|
getCookie.mockReturnValue('already-started')
|
|
41
94
|
|
|
42
|
-
renderHook(() => useStartRecommendationSession())
|
|
95
|
+
renderHook(() => useStartRecommendationSession(enabledPageProps))
|
|
43
96
|
|
|
44
97
|
await waitFor(() => {
|
|
45
98
|
expect(runStartRecommendationSession).not.toHaveBeenCalled()
|
|
46
99
|
})
|
|
47
100
|
})
|
|
48
101
|
|
|
49
|
-
it('starts a session when no session cookie is present', async () => {
|
|
102
|
+
it('starts a session when CMS enableRecommendations is true and no session cookie is present', async () => {
|
|
50
103
|
getCookie.mockReturnValue(undefined)
|
|
51
104
|
|
|
52
|
-
renderHook(() => useStartRecommendationSession())
|
|
105
|
+
renderHook(() => useStartRecommendationSession(enabledPageProps))
|
|
53
106
|
|
|
54
107
|
await waitFor(() => {
|
|
55
|
-
expect(runStartRecommendationSession).
|
|
108
|
+
expect(runStartRecommendationSession).toHaveBeenCalledTimes(1)
|
|
56
109
|
})
|
|
57
110
|
})
|
|
58
111
|
|
|
59
|
-
it('
|
|
60
|
-
storeConfigMock.experimental.enableRecommendations = false
|
|
112
|
+
it('starts a session only once when the page has multiple enabled RecommendationShelves', async () => {
|
|
61
113
|
getCookie.mockReturnValue(undefined)
|
|
62
114
|
|
|
63
|
-
|
|
115
|
+
const { rerender } = renderHook(
|
|
116
|
+
({ pageProps }) => useStartRecommendationSession(pageProps),
|
|
117
|
+
{ initialProps: { pageProps: multipleEnabledShelvesPageProps } }
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
// Simulate Layout re-renders caused by multiple shelves updating.
|
|
121
|
+
rerender({ pageProps: multipleEnabledShelvesPageProps })
|
|
122
|
+
rerender({ pageProps: multipleEnabledShelvesPageProps })
|
|
64
123
|
|
|
65
124
|
await waitFor(() => {
|
|
66
|
-
expect(runStartRecommendationSession).
|
|
125
|
+
expect(runStartRecommendationSession).toHaveBeenCalledTimes(1)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('does not start a second session when the hook mounts again without a cookie', async () => {
|
|
130
|
+
getCookie.mockReturnValue(undefined)
|
|
131
|
+
|
|
132
|
+
renderHook(() => useStartRecommendationSession(enabledPageProps))
|
|
133
|
+
|
|
134
|
+
await waitFor(() => {
|
|
135
|
+
expect(runStartRecommendationSession).toHaveBeenCalledTimes(1)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
renderHook(() => useStartRecommendationSession(enabledPageProps))
|
|
139
|
+
|
|
140
|
+
await waitFor(() => {
|
|
141
|
+
expect(runStartRecommendationSession).toHaveBeenCalledTimes(1)
|
|
67
142
|
})
|
|
68
143
|
})
|
|
69
144
|
})
|