@faststore/core 4.7.0-dev.3 → 4.7.0-dev.5
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 +101 -92
- package/CHANGELOG.md +18 -0
- package/cms/faststore/components/cms_component__recommendationshelf.jsonc +1 -7
- package/cms/faststore/my-account/README.md +2 -2
- package/cms/faststore/schema.json +1 -7
- package/discovery.config.default.js +5 -0
- package/package.json +6 -6
- package/src/Layout.tsx +3 -3
- package/src/components/account/orders/OrderDetails/section.module.scss +44 -3
- package/src/components/cms/RenderSections.tsx +20 -2
- package/src/components/sections/RecommendationShelf/README.md +25 -15
- package/src/components/sections/RecommendationShelf/RecommendationShelf.tsx +0 -2
- package/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts +0 -7
- package/src/pages/pvt/account/403.tsx +1 -0
- package/src/pages/pvt/account/404.tsx +5 -1
- package/src/pages/pvt/account/cards/index.tsx +1 -0
- package/src/pages/pvt/account/orders/[id].tsx +5 -1
- package/src/pages/pvt/account/orders/index.tsx +1 -0
- package/src/pages/pvt/account/profile.tsx +1 -0
- package/src/pages/pvt/account/security.tsx +1 -0
- package/src/pages/pvt/account/user-details.tsx +1 -0
- package/src/sdk/analytics/hooks/useStartRecommendationSession.ts +18 -13
- package/src/sdk/recommendations/useRecommendationShelf.ts +17 -15
- package/src/server/cms/myAccountDefaultSections.ts +8 -3
- package/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx +11 -32
- package/test/components/cms/RenderSections.browser.test.tsx +135 -0
- package/test/sdk/analytics/useStartRecommendationSession.test.ts +27 -58
- package/test/sdk/recommendations/useRecommendationShelf.browser.test.tsx +32 -34
- package/test/sdk/recommendations/useRecommendationShelf.sessionFallback.browser.test.tsx +110 -0
- package/test/server/cms/myAccountDefaultSections.test.ts +44 -0
- package/src/sdk/analytics/utils/hasEnabledRecommendationShelf.ts +0 -49
- package/test/sdk/analytics/hasEnabledRecommendationShelf.test.ts +0 -88
|
@@ -4,7 +4,7 @@ CMS section that renders a carousel of personalized product recommendations from
|
|
|
4
4
|
|
|
5
5
|
- **Data source:** VTEX Recommendations BFF, exposed by `@faststore/api` through the `recommendations` GraphQL query.
|
|
6
6
|
- **Registration:** global CMS component (`$componentKey: "RecommendationShelf"`), available on any page via CMS.
|
|
7
|
-
- **
|
|
7
|
+
- **Session opt-in:** personalization session starts via the `experimental.enableRecommendations` discovery feature flag (Layout, every page). When the flag is off, any mounted Recommendation Shelf starts the session as a fallback if `userId` is missing.
|
|
8
8
|
|
|
9
9
|
## Contents
|
|
10
10
|
|
|
@@ -22,7 +22,7 @@ CMS section that renders a carousel of personalized product recommendations from
|
|
|
22
22
|
|
|
23
23
|
The shelf renders products only when the following conditions are met:
|
|
24
24
|
|
|
25
|
-
1. **
|
|
25
|
+
1. **Personalization session.** Prefer enabling `experimental.enableRecommendations` in `discovery.config` so `Layout` starts the session on every page. If the flag is off, any mounted Recommendation Shelf starts the session as a fallback when `userId` is missing (cookie + in-memory lock still limit this to one attempt per browser session).
|
|
26
26
|
|
|
27
27
|
2. **Active VTEX Recommendations campaign.** Provide a valid campaign **VRN**. Example: `vrn:recommendations:my-account:rec-persona-v2:abc123`.
|
|
28
28
|
|
|
@@ -32,7 +32,19 @@ The shelf renders products only when the following conditions are met:
|
|
|
32
32
|
|
|
33
33
|
## Setup
|
|
34
34
|
|
|
35
|
-
### 1.
|
|
35
|
+
### 1. Opt into the personalization session (recommended)
|
|
36
|
+
|
|
37
|
+
In the store's `discovery.config.js` (or local override), set:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
experimental: {
|
|
41
|
+
enableRecommendations: true,
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Default is `false`. With the flag on, `Layout` calls `startRecommendationSession` on every page without inspecting CMS data. With the flag off, placing a Recommendation Shelf on a page still starts the session when `userId` is missing.
|
|
46
|
+
|
|
47
|
+
### 2. Obtain the campaign VRN
|
|
36
48
|
|
|
37
49
|
In the VTEX Recommendations admin, create or select a campaign and copy its **VRN**. The expected format is:
|
|
38
50
|
|
|
@@ -42,9 +54,9 @@ vrn:recommendations:<account>:<campaign-type>:<campaign-id>
|
|
|
42
54
|
|
|
43
55
|
Supported `<campaign-type>` values are listed under [Campaign types (VRN) and context](#campaign-types-vrn-and-context).
|
|
44
56
|
|
|
45
|
-
###
|
|
57
|
+
### 3. Add Recommendation Shelf via CMS
|
|
46
58
|
|
|
47
|
-
Add **Recommendation Shelf** to the target page (home, PDP, PLP, cart, and so on)
|
|
59
|
+
Add **Recommendation Shelf** to the target page (home, PDP, PLP, cart, and so on) and configure at least **Campaign VRN** (required).
|
|
48
60
|
|
|
49
61
|
## CMS configuration
|
|
50
62
|
|
|
@@ -52,7 +64,6 @@ Schema: `cms/faststore/components/cms_component__recommendationshelf.jsonc`.
|
|
|
52
64
|
|
|
53
65
|
| CMS property | Key | Type | Required | Default | Description |
|
|
54
66
|
| --- | --- | --- | --- | --- | --- |
|
|
55
|
-
| Enable recommendations? | `enableRecommendations` | boolean | No | `false` | Opt-in that starts the personalization session (via Layout) and allows this shelf to fetch recommendations. |
|
|
56
67
|
| Title | `title` | string | No | — | Overrides the shelf title. Falls back to the campaign title. If both are empty, the heading is not rendered. |
|
|
57
68
|
| Campaign VRN | `campaignVrn` | string | **Yes** | — | Recommendation campaign VRN. Validated by regex (see campaign types below). |
|
|
58
69
|
| Items context | `itemsContext` | `PDP` \| `CART` | No | `PDP` | Source of products used as request context. Applies only to context-based campaigns. |
|
|
@@ -92,9 +103,9 @@ Context-based campaigns (cross-sell, similar items, visual similarity, next inte
|
|
|
92
103
|
|
|
93
104
|
All recommendation work runs client-side after hydration:
|
|
94
105
|
|
|
95
|
-
1. **Session start (
|
|
106
|
+
1. **Session start (feature flag + shelf fallback)** — `Layout` calls `useStartRecommendationSession()` once. The hook starts the session when `experimental.enableRecommendations` is `true` (no CMS lookup). When the flag is `false`, `useRecommendationShelf()` (used by every RecommendationShelf) calls the same hook as a fallback while `userId` is missing (`undefined` or `null`), in parallel with cookie retry. Cookie (`vtex-rec-user-start-session`) + in-memory lock ensure at most one attempt per browser session. If `userId` is already available, the shelf does not start the session.
|
|
96
107
|
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** —
|
|
108
|
+
3. **Request arguments** — `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.
|
|
98
109
|
4. **Fetch** — `useRecommendations()` runs `ClientRecommendationsQuery` and returns `products`, `correlationId`, and `campaign`.
|
|
99
110
|
5. **Render** — shows `ProductShelfSkeleton` while loading; returns `null` on error or empty results; otherwise renders the carousel and optional heading.
|
|
100
111
|
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`.
|
|
@@ -107,6 +118,7 @@ Related files:
|
|
|
107
118
|
- Shelf: `src/components/sections/RecommendationShelf/`
|
|
108
119
|
- Hook: `src/sdk/analytics/hooks/useStartRecommendationSession.ts`
|
|
109
120
|
- Layout: `src/Layout.tsx`
|
|
121
|
+
- Flag: `discovery.config.default.js` → `experimental.enableRecommendations`
|
|
110
122
|
|
|
111
123
|
## Code-level overrides
|
|
112
124
|
|
|
@@ -116,7 +128,6 @@ In addition to CMS props, the component accepts code-level overrides (not expose
|
|
|
116
128
|
import { RecommendationShelf } from 'src/components/sections/RecommendationShelf'
|
|
117
129
|
|
|
118
130
|
<RecommendationShelf
|
|
119
|
-
enableRecommendations
|
|
120
131
|
campaignVrn="vrn:recommendations:my-account:rec-cross-v2:abc123"
|
|
121
132
|
itemsContext="PDP"
|
|
122
133
|
ProductCard={MyCustomCard}
|
|
@@ -143,7 +154,7 @@ As a global component, the shelf can be placed on any CMS page. Suggested placem
|
|
|
143
154
|
- **Cart:** cross-sell with `itemsContext: CART`.
|
|
144
155
|
- **PLP:** context-agnostic campaigns.
|
|
145
156
|
|
|
146
|
-
|
|
157
|
+
Prefer enabling `experimental.enableRecommendations` so the session is ready on every page. Stores without the flag and without a Recommendation Shelf never call `startRecommendationSession`.
|
|
147
158
|
|
|
148
159
|
## Privacy and cache
|
|
149
160
|
|
|
@@ -155,11 +166,10 @@ The personalization session runs only when at least one shelf has **Enable recom
|
|
|
155
166
|
|
|
156
167
|
**The shelf does not render.** Check, in order:
|
|
157
168
|
|
|
158
|
-
1. Is
|
|
159
|
-
2. Is `
|
|
160
|
-
3.
|
|
161
|
-
4.
|
|
162
|
-
5. Did the campaign return products? An empty response renders nothing by design.
|
|
169
|
+
1. Is `campaignVrn` valid and is the campaign active?
|
|
170
|
+
2. Is the `vtex-rec-user-id` cookie available? Without a user id, the shelf does not fetch. With the discovery flag off, confirm the shelf fallback started the session (`vtex-rec-user-start-session`).
|
|
171
|
+
3. For context-based campaigns, is product context available (`PDP` on a product page, or a non-empty cart for `CART`)?
|
|
172
|
+
4. Did the campaign return products? An empty response renders nothing by design.
|
|
163
173
|
|
|
164
174
|
**Skeleton never resolves.** Inspect the console and the GraphQL `recommendations` response for pending or failing requests.
|
|
165
175
|
|
|
@@ -21,7 +21,6 @@ export function RecommendationShelf<
|
|
|
21
21
|
>({
|
|
22
22
|
title,
|
|
23
23
|
campaignVrn,
|
|
24
|
-
enableRecommendations = false,
|
|
25
24
|
itemsContext = 'PDP',
|
|
26
25
|
ProductCard,
|
|
27
26
|
mapProductToProductCard,
|
|
@@ -56,7 +55,6 @@ export function RecommendationShelf<
|
|
|
56
55
|
const { items, isLoading, error, campaign, correlationId } =
|
|
57
56
|
useRecommendationShelf({
|
|
58
57
|
campaignVrn,
|
|
59
|
-
enableRecommendations,
|
|
60
58
|
itemsContext,
|
|
61
59
|
})
|
|
62
60
|
|
|
@@ -25,13 +25,6 @@ export type RecommendationShelfProps<
|
|
|
25
25
|
> = {
|
|
26
26
|
readonly title?: string
|
|
27
27
|
readonly campaignVrn: string
|
|
28
|
-
/**
|
|
29
|
-
* Opt-in for VTEX Recommendations on this shelf. When `true`, Layout starts
|
|
30
|
-
* the personalization session (`useStartRecommendationSession`) and this
|
|
31
|
-
* shelf fetches recommendations. When `false` (default), neither happens.
|
|
32
|
-
* @default false
|
|
33
|
-
*/
|
|
34
|
-
readonly enableRecommendations?: boolean
|
|
35
28
|
/**
|
|
36
29
|
* Where to read the products used as context for the recommendation request:
|
|
37
30
|
* - `'PDP'`: the current product detail page product.
|
|
@@ -57,7 +57,11 @@ function Page({
|
|
|
57
57
|
|
|
58
58
|
<Layout accountName={accountName}>
|
|
59
59
|
{sections && sections.length > 0 && (
|
|
60
|
-
<RenderSectionsBase
|
|
60
|
+
<RenderSectionsBase
|
|
61
|
+
skipLazyLoading
|
|
62
|
+
sections={sections}
|
|
63
|
+
components={COMPONENTS}
|
|
64
|
+
/>
|
|
61
65
|
)}
|
|
62
66
|
</Layout>
|
|
63
67
|
</RenderSections>
|
|
@@ -76,6 +76,7 @@ export default function OrderDetailsPage({
|
|
|
76
76
|
<OrderDetailsPageShell>
|
|
77
77
|
<main data-fs-order-details-content>
|
|
78
78
|
<RenderSectionsBase
|
|
79
|
+
skipLazyLoading
|
|
79
80
|
sections={pageSections}
|
|
80
81
|
components={ACCOUNT_COMPONENTS}
|
|
81
82
|
/>
|
|
@@ -408,7 +409,10 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
408
409
|
navigationLabels: navigationData as AccountNavigationLabels,
|
|
409
410
|
accountPageData: {
|
|
410
411
|
order: orderDetails.data.userOrder,
|
|
411
|
-
|
|
412
|
+
// Yields undefined when no section carries order-status labels — the
|
|
413
|
+
// case for a store with no CMS content, since the default sections are
|
|
414
|
+
// created with empty data. getServerSideProps cannot serialize it.
|
|
415
|
+
...(orderStatusLabels ? { orderStatusLabels } : {}),
|
|
412
416
|
},
|
|
413
417
|
pageSections,
|
|
414
418
|
isRepresentative,
|
|
@@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { gql } from '@faststore/core/api'
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import storeConfig from 'discovery.config'
|
|
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,9 +20,9 @@ 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
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// In-memory lock so Layout re-renders / shelf fallback cannot start more than
|
|
24
|
+
// one session before the cookie lands. Survives effect cleanup / Strict Mode
|
|
25
|
+
// remounts for this JS realm.
|
|
26
26
|
let hasRequestedStartRecommendationSession = false
|
|
27
27
|
|
|
28
28
|
/** @internal Test-only: clears the in-memory session-start lock. */
|
|
@@ -34,14 +34,14 @@ export function resetStartRecommendationSessionLock() {
|
|
|
34
34
|
* Starts the anonymous recommendation/personalization session once per browser
|
|
35
35
|
* session.
|
|
36
36
|
*
|
|
37
|
-
* Mounted from `Layout`
|
|
38
|
-
* the
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* Mounted from `Layout` (gated by `experimental.enableRecommendations`) so it
|
|
38
|
+
* runs on every page when the store opts in via discovery config. Also used as
|
|
39
|
+
* a fallback from `RecommendationShelf` when the flag is off and `userId` is
|
|
40
|
+
* missing (including while lookup is still pending) — pass `enabled` to
|
|
41
|
+
* override the feature flag for that call site.
|
|
41
42
|
*
|
|
42
|
-
* Multiple
|
|
43
|
-
*
|
|
44
|
-
* session cookie.
|
|
43
|
+
* Multiple callers still produce a single mutation: the in-memory lock plus the
|
|
44
|
+
* session cookie ensure at most one attempt per browser session.
|
|
45
45
|
*
|
|
46
46
|
* The product view is no longer reported through a mutation here: the PDP now
|
|
47
47
|
* exposes the product data as `<meta property="product:*">` tags, which the
|
|
@@ -49,9 +49,14 @@ export function resetStartRecommendationSessionLock() {
|
|
|
49
49
|
*
|
|
50
50
|
* All work runs client-side in effects (after hydration), so it doesn't affect
|
|
51
51
|
* SSR/TTFB or Lighthouse render metrics.
|
|
52
|
+
*
|
|
53
|
+
* @param enabled Optional override. When omitted, uses
|
|
54
|
+
* `storeConfig.experimental.enableRecommendations`. When provided (e.g. by
|
|
55
|
+
* RecommendationShelf fallback), that boolean wins.
|
|
52
56
|
*/
|
|
53
|
-
export function useStartRecommendationSession(
|
|
54
|
-
const
|
|
57
|
+
export function useStartRecommendationSession(enabled?: boolean) {
|
|
58
|
+
const flagEnabled = storeConfig.experimental?.enableRecommendations === true
|
|
59
|
+
const enableRecommendations = enabled ?? flagEnabled
|
|
55
60
|
|
|
56
61
|
const [runStartRecommendationSession] = useLazyQuery<
|
|
57
62
|
StartRecommendationSessionData,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { usePDP } from '@faststore/core'
|
|
2
2
|
import { useMemo } from 'react'
|
|
3
3
|
|
|
4
|
+
import { useStartRecommendationSession } from 'src/sdk/analytics/hooks/useStartRecommendationSession'
|
|
4
5
|
import { useCart } from 'src/sdk/cart'
|
|
5
6
|
|
|
6
7
|
import type { ItemContext } from './types'
|
|
@@ -50,12 +51,6 @@ export function getRecommendationArguments(
|
|
|
50
51
|
|
|
51
52
|
export type UseRecommendationShelfArgs = {
|
|
52
53
|
readonly campaignVrn: string
|
|
53
|
-
/**
|
|
54
|
-
* Opt-in for VTEX Recommendations. When `false` (default) the campaign is
|
|
55
|
-
* never requested. The anonymous user id is still resolved, but it goes
|
|
56
|
-
* unused and never leaves the browser.
|
|
57
|
-
*/
|
|
58
|
-
readonly enableRecommendations?: boolean
|
|
59
54
|
/**
|
|
60
55
|
* Where to read the products used as context for the recommendation request.
|
|
61
56
|
* @default 'PDP'
|
|
@@ -79,14 +74,25 @@ const EMPTY_ITEMS: RecommendationResponse['products'] = []
|
|
|
79
74
|
* campaign. It renders nothing — presentation lives in the shelf components
|
|
80
75
|
* that consume it, so a new surface can reuse the campaign rules instead of
|
|
81
76
|
* reimplementing them.
|
|
77
|
+
*
|
|
78
|
+
* When `userId` is missing (`undefined` or `null`), starts the personalization
|
|
79
|
+
* session as a fallback in parallel with cookie retry (Layout already starts
|
|
80
|
+
* it when `experimental.enableRecommendations` is on). Cookie + in-memory lock
|
|
81
|
+
* keep the mutation to one attempt per browser session.
|
|
82
82
|
*/
|
|
83
83
|
export function useRecommendationShelf({
|
|
84
84
|
campaignVrn,
|
|
85
|
-
enableRecommendations = false,
|
|
86
85
|
itemsContext = 'PDP',
|
|
87
86
|
}: UseRecommendationShelfArgs): UseRecommendationShelfResult {
|
|
88
87
|
const userId = useRecommendationUserId(campaignVrn)
|
|
89
88
|
|
|
89
|
+
// Fallback when Layout did not start the session (flag off): start in
|
|
90
|
+
// parallel with cookie lookup. `undefined` and `null` both mean no id yet;
|
|
91
|
+
// waiting for `null` exhausts the retry budget before the mutation can set
|
|
92
|
+
// `vtex-rec-user-id`, so the first page would never fetch. Cookie + lock
|
|
93
|
+
// already no-op when a session exists.
|
|
94
|
+
useStartRecommendationSession(!userId)
|
|
95
|
+
|
|
90
96
|
const { data: productDetailPage } = usePDP()
|
|
91
97
|
const { items: cartItems } = useCart()
|
|
92
98
|
|
|
@@ -108,14 +114,10 @@ export function useRecommendationShelf({
|
|
|
108
114
|
return pdpProduct ? [pdpProduct] : []
|
|
109
115
|
}, [itemsContext, cartItems, productDetailPage])
|
|
110
116
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
userId,
|
|
116
|
-
contextProducts,
|
|
117
|
-
})
|
|
118
|
-
: null
|
|
117
|
+
const recommendationArgs = getRecommendationArguments(campaignVrn, {
|
|
118
|
+
userId,
|
|
119
|
+
contextProducts,
|
|
120
|
+
})
|
|
119
121
|
|
|
120
122
|
const { data, isLoading, error } = useRecommendations(recommendationArgs)
|
|
121
123
|
|
|
@@ -20,16 +20,21 @@ const DEFAULT_SECTION_KEYS: Record<NativeMyAccountContentType, string[]> = {
|
|
|
20
20
|
myAccountProfile: ['AccountNavigation', 'AccountProfile'],
|
|
21
21
|
myAccountOrders: ['AccountNavigation', 'AccountOrdersList'],
|
|
22
22
|
myAccountCards: ['AccountNavigation', 'AccountListCards'],
|
|
23
|
+
// Mirrors the render order of the `@deprecated OrderDetails.tsx` component,
|
|
24
|
+
// which is the design reference available in the repo. The delivery option
|
|
25
|
+
// accordions render inside AccountOrderDelivery, so they sit right after the
|
|
26
|
+
// compact delivery card instead of after the summary. Known deviation: fixing
|
|
27
|
+
// it would mean splitting AccountOrderDelivery into two CMS sections.
|
|
23
28
|
myAccountOrderDetails: [
|
|
24
29
|
'AccountNavigation',
|
|
25
30
|
'AccountOrderDetails',
|
|
31
|
+
'AccountOrderOrderedBy',
|
|
32
|
+
'AccountOrderDelivery',
|
|
26
33
|
'AccountOrderStatus',
|
|
27
34
|
'AccountOrderPayment',
|
|
28
|
-
'AccountOrderDelivery',
|
|
29
35
|
'AccountOrderSummary',
|
|
30
|
-
'AccountOrderOrderedBy',
|
|
31
|
-
'AccountOrderBudgets',
|
|
32
36
|
'AccountOrderMoreInfo',
|
|
37
|
+
'AccountOrderBudgets',
|
|
33
38
|
],
|
|
34
39
|
myAccountUserDetails: ['AccountNavigation', 'AccountUserDetails'],
|
|
35
40
|
myAccountSecurity: ['AccountNavigation', 'AccountSecurity'],
|
|
@@ -41,6 +41,10 @@ vi.mock('src/sdk/recommendations/useRecommendationUserId', () => ({
|
|
|
41
41
|
useRecommendationUserId,
|
|
42
42
|
}))
|
|
43
43
|
|
|
44
|
+
vi.mock('src/sdk/analytics/hooks/useStartRecommendationSession', () => ({
|
|
45
|
+
useStartRecommendationSession: vi.fn(),
|
|
46
|
+
}))
|
|
47
|
+
|
|
44
48
|
import { RecommendationShelf } from 'src/components/sections/RecommendationShelf/RecommendationShelf'
|
|
45
49
|
|
|
46
50
|
const CAMPAIGN_VRN = 'vrn:recommendations:acc:rec-top-items-v2:campaign-1'
|
|
@@ -71,7 +75,7 @@ afterEach(() => {
|
|
|
71
75
|
})
|
|
72
76
|
|
|
73
77
|
describe('RecommendationShelf', () => {
|
|
74
|
-
it('renders a product card per recommended product
|
|
78
|
+
it('renders a product card per recommended product', async () => {
|
|
75
79
|
useRecommendations.mockReturnValue({
|
|
76
80
|
data: recommendationData,
|
|
77
81
|
isLoading: false,
|
|
@@ -79,7 +83,7 @@ describe('RecommendationShelf', () => {
|
|
|
79
83
|
})
|
|
80
84
|
|
|
81
85
|
const { getAllByTestId, getByText } = render(
|
|
82
|
-
<RecommendationShelf campaignVrn={CAMPAIGN_VRN}
|
|
86
|
+
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} />
|
|
83
87
|
)
|
|
84
88
|
|
|
85
89
|
await waitFor(() => {
|
|
@@ -98,9 +102,7 @@ describe('RecommendationShelf', () => {
|
|
|
98
102
|
error: null,
|
|
99
103
|
})
|
|
100
104
|
|
|
101
|
-
render(
|
|
102
|
-
<RecommendationShelf campaignVrn={CROSS_SELL_VRN} enableRecommendations />
|
|
103
|
-
)
|
|
105
|
+
render(<RecommendationShelf campaignVrn={CROSS_SELL_VRN} />)
|
|
104
106
|
|
|
105
107
|
await waitFor(() => {
|
|
106
108
|
const lastArgs = useRecommendations.mock.calls.at(-1)?.[0]
|
|
@@ -120,11 +122,7 @@ describe('RecommendationShelf', () => {
|
|
|
120
122
|
})
|
|
121
123
|
|
|
122
124
|
const { getByText } = render(
|
|
123
|
-
<RecommendationShelf
|
|
124
|
-
campaignVrn={CAMPAIGN_VRN}
|
|
125
|
-
enableRecommendations
|
|
126
|
-
title="Custom title"
|
|
127
|
-
/>
|
|
125
|
+
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} title="Custom title" />
|
|
128
126
|
)
|
|
129
127
|
|
|
130
128
|
await waitFor(() => {
|
|
@@ -140,25 +138,6 @@ describe('RecommendationShelf', () => {
|
|
|
140
138
|
error: null,
|
|
141
139
|
})
|
|
142
140
|
|
|
143
|
-
const { container, queryByTestId } = render(
|
|
144
|
-
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} enableRecommendations />
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
await waitFor(() => {
|
|
148
|
-
const lastArgs = useRecommendations.mock.calls.at(-1)?.[0]
|
|
149
|
-
expect(lastArgs).toBeNull()
|
|
150
|
-
})
|
|
151
|
-
expect(queryByTestId('product-card')).toBeNull()
|
|
152
|
-
expect(container).toBeEmptyDOMElement()
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('skips the fetch when recommendations are disabled (default)', async () => {
|
|
156
|
-
useRecommendations.mockReturnValue({
|
|
157
|
-
data: undefined,
|
|
158
|
-
isLoading: false,
|
|
159
|
-
error: null,
|
|
160
|
-
})
|
|
161
|
-
|
|
162
141
|
const { container, queryByTestId } = render(
|
|
163
142
|
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} />
|
|
164
143
|
)
|
|
@@ -179,7 +158,7 @@ describe('RecommendationShelf', () => {
|
|
|
179
158
|
})
|
|
180
159
|
|
|
181
160
|
const { container } = render(
|
|
182
|
-
<RecommendationShelf campaignVrn={CAMPAIGN_VRN}
|
|
161
|
+
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} />
|
|
183
162
|
)
|
|
184
163
|
|
|
185
164
|
expect(container).toBeEmptyDOMElement()
|
|
@@ -193,7 +172,7 @@ describe('RecommendationShelf', () => {
|
|
|
193
172
|
})
|
|
194
173
|
|
|
195
174
|
const { container, queryByTestId } = render(
|
|
196
|
-
<RecommendationShelf campaignVrn={CAMPAIGN_VRN}
|
|
175
|
+
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} />
|
|
197
176
|
)
|
|
198
177
|
|
|
199
178
|
expect(queryByTestId('product-card')).toBeNull()
|
|
@@ -208,7 +187,7 @@ describe('RecommendationShelf', () => {
|
|
|
208
187
|
})
|
|
209
188
|
|
|
210
189
|
const { getByTestId } = render(
|
|
211
|
-
<RecommendationShelf campaignVrn={CAMPAIGN_VRN}
|
|
190
|
+
<RecommendationShelf campaignVrn={CAMPAIGN_VRN} />
|
|
212
191
|
)
|
|
213
192
|
|
|
214
193
|
expect(getByTestId('skeleton').getAttribute('data-loading')).toBe('true')
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { UIProvider } from '@faststore/ui'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { renderToString } from 'react-dom/server'
|
|
4
|
+
import { beforeAll, describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { RenderSectionsBase } from 'src/components/cms/RenderSections'
|
|
7
|
+
import { getDefaultMyAccountSections } from 'src/server/cms/myAccountDefaultSections'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* First-paint behavior of the My Account pages.
|
|
11
|
+
*
|
|
12
|
+
* Sections rendered through `RenderSectionsBase` go through `ViewportObserver`,
|
|
13
|
+
* which emits an 823px-tall placeholder instead of the section content until the
|
|
14
|
+
* element intersects the viewport. That is correct for the public storefront and
|
|
15
|
+
* wrong for My Account, which is authenticated, SSR and above the fold.
|
|
16
|
+
*
|
|
17
|
+
* `skipLazyLoading` opts a whole render pass out of it.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Never-firing observer — reproduces the pre-intersection (first paint) state. */
|
|
21
|
+
class NeverIntersectingObserver implements IntersectionObserver {
|
|
22
|
+
readonly root = null
|
|
23
|
+
readonly rootMargin = ''
|
|
24
|
+
readonly thresholds = [] as unknown as ReadonlyArray<number>
|
|
25
|
+
observe() {}
|
|
26
|
+
unobserve() {}
|
|
27
|
+
disconnect() {}
|
|
28
|
+
takeRecords() {
|
|
29
|
+
return []
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
beforeAll(() => {
|
|
34
|
+
window.IntersectionObserver =
|
|
35
|
+
NeverIntersectingObserver as unknown as typeof IntersectionObserver
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const orderDetailsSections = getDefaultMyAccountSections(
|
|
39
|
+
'myAccountOrderDetails'
|
|
40
|
+
).filter((section) => section.$componentKey !== 'AccountNavigation')
|
|
41
|
+
|
|
42
|
+
const markerComponents = Object.fromEntries(
|
|
43
|
+
orderDetailsSections.map((section) => [
|
|
44
|
+
section.$componentKey,
|
|
45
|
+
() => <div>{section.$componentKey}</div>,
|
|
46
|
+
])
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
describe('skipLazyLoading', () => {
|
|
50
|
+
it('renders section content immediately instead of a placeholder', () => {
|
|
51
|
+
const { container } = render(
|
|
52
|
+
<UIProvider>
|
|
53
|
+
<RenderSectionsBase
|
|
54
|
+
skipLazyLoading
|
|
55
|
+
sections={[{ name: 'AccountOrderStatus', data: {} }]}
|
|
56
|
+
components={{ AccountOrderStatus: () => <div>status</div> }}
|
|
57
|
+
/>
|
|
58
|
+
</UIProvider>
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
expect(screen.getByText('status')).toBeTruthy()
|
|
62
|
+
expect(container.querySelector('[data-store-section-name]')).toBeNull()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('renders every default order-details section on first paint', () => {
|
|
66
|
+
const { container } = render(
|
|
67
|
+
<UIProvider>
|
|
68
|
+
<RenderSectionsBase
|
|
69
|
+
skipLazyLoading
|
|
70
|
+
sections={orderDetailsSections}
|
|
71
|
+
components={markerComponents}
|
|
72
|
+
/>
|
|
73
|
+
</UIProvider>
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
for (const section of orderDetailsSections) {
|
|
77
|
+
expect(screen.getByText(section.$componentKey)).toBeTruthy()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
expect(
|
|
81
|
+
container.querySelectorAll('[data-store-section-name]')
|
|
82
|
+
).toHaveLength(0)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('covers store-authored sections, not only the framework defaults', () => {
|
|
86
|
+
render(
|
|
87
|
+
<UIProvider>
|
|
88
|
+
<RenderSectionsBase
|
|
89
|
+
skipLazyLoading
|
|
90
|
+
sections={[{ name: 'StoreCustomAccountSection', data: {} }]}
|
|
91
|
+
components={{
|
|
92
|
+
StoreCustomAccountSection: () => <div>custom</div>,
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
</UIProvider>
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
expect(screen.getByText('custom')).toBeTruthy()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('puts the section content in the SSR HTML, not the placeholder', () => {
|
|
102
|
+
const html = renderToString(
|
|
103
|
+
<UIProvider>
|
|
104
|
+
<RenderSectionsBase
|
|
105
|
+
skipLazyLoading
|
|
106
|
+
sections={orderDetailsSections}
|
|
107
|
+
components={markerComponents}
|
|
108
|
+
/>
|
|
109
|
+
</UIProvider>
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
expect(html).toContain('AccountOrderStatus')
|
|
113
|
+
expect(html).not.toContain('data-store-section-name')
|
|
114
|
+
expect(html).not.toContain('823')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('preserves lazy loading when the flag is absent', () => {
|
|
118
|
+
const { container } = render(
|
|
119
|
+
<UIProvider>
|
|
120
|
+
<RenderSectionsBase
|
|
121
|
+
sections={[{ name: 'AccountOrderStatus', data: {} }]}
|
|
122
|
+
components={{ AccountOrderStatus: () => <div>status</div> }}
|
|
123
|
+
/>
|
|
124
|
+
</UIProvider>
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
expect(screen.queryByText('status')).toBeNull()
|
|
128
|
+
|
|
129
|
+
const placeholder = container.querySelector(
|
|
130
|
+
'[data-store-section-name="AccountOrderStatus"]'
|
|
131
|
+
) as HTMLElement | null
|
|
132
|
+
|
|
133
|
+
expect(placeholder?.style.height).toBe('823px')
|
|
134
|
+
})
|
|
135
|
+
})
|