@faststore/core 4.7.1-dev.1 → 4.8.0-dev.0

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.
Files changed (23) hide show
  1. package/.turbo/turbo-generate.log +3 -3
  2. package/.turbo/turbo-test.log +104 -104
  3. package/CHANGELOG.md +21 -0
  4. package/cms/faststore/components/cms_component__cartsidebar.jsonc +74 -0
  5. package/cms/faststore/schema.json +78 -0
  6. package/cms/faststore/sections.json +78 -0
  7. package/package.json +6 -6
  8. package/src/components/cart/CartRecommendationShelf/CartRecommendationProductCard.tsx +88 -0
  9. package/src/components/cart/CartRecommendationShelf/CartRecommendationShelf.tsx +110 -0
  10. package/src/components/cart/CartRecommendationShelf/index.ts +4 -0
  11. package/src/components/cart/CartRecommendationShelf/section.module.scss +124 -0
  12. package/src/components/cart/CartSidebar/CartSidebar.tsx +47 -1
  13. package/src/components/product/ProductCard/ProductCard.tsx +5 -3
  14. package/src/components/sections/RecommendationShelf/README.md +74 -6
  15. package/src/components/sections/RecommendationShelf/RecommendationShelf.tsx +15 -3
  16. package/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts +8 -2
  17. package/src/components/sections/RecommendationShelf/index.ts +7 -1
  18. package/src/sdk/recommendations/useRecommendationShelf.ts +2 -2
  19. package/test/components/CartRecommendationShelf/CartRecommendationProductCard.browser.test.tsx +125 -0
  20. package/test/components/CartRecommendationShelf/CartRecommendationShelf.browser.test.tsx +286 -0
  21. package/test/components/CartSidebar/CartSidebar.browser.test.tsx +209 -0
  22. package/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx +19 -0
  23. package/test/sdk/recommendations/useRecommendationShelf.browser.test.tsx +1 -1
@@ -0,0 +1,209 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+ import { render } from '@testing-library/react'
3
+ import React from 'react'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ vi.mock('next/dynamic', () => ({
7
+ default: (loader: () => Promise<unknown>) => {
8
+ const source = Function.prototype.toString.call(loader)
9
+ const chunk = /webpackChunkName:\s*"([^"]+)"/.exec(source)?.[1] ?? 'dynamic'
10
+
11
+ return function DynamicStub({
12
+ children,
13
+ ...props
14
+ }: React.PropsWithChildren<Record<string, unknown>>) {
15
+ const serializableProps = Object.fromEntries(
16
+ Object.entries(props).filter(
17
+ ([, value]) =>
18
+ value == null ||
19
+ typeof value === 'string' ||
20
+ typeof value === 'number' ||
21
+ typeof value === 'boolean'
22
+ )
23
+ )
24
+
25
+ return React.createElement(
26
+ 'div',
27
+ {
28
+ 'data-testid': chunk,
29
+ 'data-props': JSON.stringify(serializableProps),
30
+ },
31
+ children
32
+ )
33
+ }
34
+ },
35
+ }))
36
+
37
+ const useUI = vi.hoisted(() => vi.fn())
38
+ vi.mock('@faststore/ui', () => ({
39
+ Icon: ({ name }: { name?: string }) =>
40
+ React.createElement('span', { 'data-testid': 'icon', 'data-name': name }),
41
+ useFadeEffect: () => ({ fadeOut: vi.fn() }),
42
+ useUI,
43
+ }))
44
+
45
+ const useCart = vi.hoisted(() => vi.fn())
46
+ vi.mock('src/sdk/cart', () => ({ useCart }))
47
+
48
+ vi.mock('src/sdk/cart/useCheckoutButton', () => ({
49
+ useCheckoutButton: () => ({
50
+ onClick: vi.fn(),
51
+ disabled: false,
52
+ 'data-testid': 'checkout-button',
53
+ }),
54
+ }))
55
+
56
+ vi.mock('src/sdk/session', () => ({
57
+ useSession: () => ({ currency: { code: 'USD' } }),
58
+ }))
59
+
60
+ vi.mock('@faststore/sdk', () => ({
61
+ sendAnalyticsEvent: vi.fn(),
62
+ }))
63
+
64
+ import CartSidebar from 'src/components/cart/CartSidebar'
65
+
66
+ const CAMPAIGN_VRN = 'vrn:recommendations:acc:rec-cross-v2:campaign-1'
67
+
68
+ const cartItem = {
69
+ id: 'item-1',
70
+ quantity: 1,
71
+ price: 100,
72
+ listPrice: 120,
73
+ itemOffered: {
74
+ sku: 'sku-1',
75
+ name: 'SKU 1',
76
+ gtin: 'gtin-1',
77
+ brand: { name: 'Brand' },
78
+ isVariantOf: { productGroupID: 'pg-1', name: 'Product 1' },
79
+ },
80
+ }
81
+
82
+ const sidebarProps = {
83
+ title: 'Your cart',
84
+ alert: {
85
+ icon: { icon: 'Truck', alt: 'Truck' },
86
+ text: 'Free shipping',
87
+ },
88
+ emptyCart: {
89
+ title: 'Your Cart is empty',
90
+ buttonLabel: 'Start Shopping',
91
+ },
92
+ checkoutButton: {
93
+ label: 'Checkout',
94
+ loadingLabel: 'Loading...',
95
+ icon: { icon: 'ArrowRight', alt: 'Arrow Right' },
96
+ },
97
+ quantitySelector: { useUnitMultiplier: false },
98
+ }
99
+
100
+ const enabledRecommendations = {
101
+ shouldDisplayRecommendationShelf: true,
102
+ campaignVrn: CAMPAIGN_VRN,
103
+ title: 'You may also like',
104
+ }
105
+
106
+ function filledCart() {
107
+ useCart.mockReturnValue({
108
+ items: [cartItem],
109
+ gifts: [],
110
+ totalItems: 1,
111
+ isValidating: false,
112
+ subTotal: 100,
113
+ total: 100,
114
+ subTotalWithTaxes: 110,
115
+ totalWithTaxes: 110,
116
+ })
117
+ }
118
+
119
+ function emptyCart() {
120
+ useCart.mockReturnValue({
121
+ items: [],
122
+ gifts: [],
123
+ totalItems: 0,
124
+ isValidating: false,
125
+ subTotal: 0,
126
+ total: 0,
127
+ subTotalWithTaxes: 0,
128
+ totalWithTaxes: 0,
129
+ })
130
+ }
131
+
132
+ beforeEach(() => {
133
+ useUI.mockReturnValue({ cart: true, closeCart: vi.fn() })
134
+ filledCart()
135
+ })
136
+
137
+ afterEach(() => {
138
+ vi.clearAllMocks()
139
+ })
140
+
141
+ describe('CartSidebar recommendation shelf gate', () => {
142
+ it('does not mount the shelf when the display toggle is off', () => {
143
+ const { queryByTestId } = render(
144
+ <CartSidebar
145
+ {...sidebarProps}
146
+ recommendations={{
147
+ shouldDisplayRecommendationShelf: false,
148
+ campaignVrn: CAMPAIGN_VRN,
149
+ }}
150
+ />
151
+ )
152
+
153
+ expect(queryByTestId('CartRecommendationShelf')).toBeNull()
154
+ expect(queryByTestId('OrderSummary')).toBeTruthy()
155
+ })
156
+
157
+ it('does not mount the shelf when the toggle is on but campaign VRN is missing', () => {
158
+ const { queryByTestId } = render(
159
+ <CartSidebar
160
+ {...sidebarProps}
161
+ recommendations={{ shouldDisplayRecommendationShelf: true }}
162
+ />
163
+ )
164
+
165
+ expect(queryByTestId('CartRecommendationShelf')).toBeNull()
166
+ })
167
+
168
+ it('does not mount the shelf on an empty cart, even with the toggle on', () => {
169
+ emptyCart()
170
+
171
+ const { queryByTestId } = render(
172
+ <CartSidebar {...sidebarProps} recommendations={enabledRecommendations} />
173
+ )
174
+
175
+ expect(queryByTestId('EmptyCart')).toBeTruthy()
176
+ expect(queryByTestId('CartRecommendationShelf')).toBeNull()
177
+ expect(queryByTestId('OrderSummary')).toBeNull()
178
+ })
179
+
180
+ it('renders the shelf before OrderSummary when the cart has items', () => {
181
+ const { getByTestId } = render(
182
+ <CartSidebar {...sidebarProps} recommendations={enabledRecommendations} />
183
+ )
184
+
185
+ const footerChildren = Array.from(
186
+ getByTestId('UICartSidebarFooter').children
187
+ )
188
+
189
+ expect(footerChildren[0]).toHaveAttribute(
190
+ 'data-testid',
191
+ 'CartRecommendationShelf'
192
+ )
193
+ expect(footerChildren[1]).toHaveAttribute('data-testid', 'OrderSummary')
194
+ })
195
+
196
+ it('omits shouldDisplayRecommendationShelf from the shelf props', () => {
197
+ const { getByTestId } = render(
198
+ <CartSidebar {...sidebarProps} recommendations={enabledRecommendations} />
199
+ )
200
+
201
+ const props = JSON.parse(
202
+ getByTestId('CartRecommendationShelf').getAttribute('data-props') ?? '{}'
203
+ ) as Record<string, unknown>
204
+
205
+ expect(props).not.toHaveProperty('shouldDisplayRecommendationShelf')
206
+ expect(props.campaignVrn).toBe(CAMPAIGN_VRN)
207
+ expect(props.title).toBe('You may also like')
208
+ })
209
+ })
@@ -75,6 +75,25 @@ afterEach(() => {
75
75
  })
76
76
 
77
77
  describe('RecommendationShelf', () => {
78
+ it('forwards extra props onto the root section', async () => {
79
+ useRecommendations.mockReturnValue({
80
+ data: recommendationData,
81
+ isLoading: false,
82
+ error: null,
83
+ })
84
+
85
+ const { getByTestId } = render(
86
+ <RecommendationShelf
87
+ campaignVrn={CAMPAIGN_VRN}
88
+ data-testid="recommendation-shelf"
89
+ />
90
+ )
91
+
92
+ await waitFor(() => {
93
+ expect(getByTestId('recommendation-shelf').tagName).toBe('SECTION')
94
+ })
95
+ })
96
+
78
97
  it('renders a product card per recommended product', async () => {
79
98
  useRecommendations.mockReturnValue({
80
99
  data: recommendationData,
@@ -151,7 +151,7 @@ describe('useRecommendationShelf', () => {
151
151
  })
152
152
  })
153
153
 
154
- it('follows the live cart context when not frozen', () => {
154
+ it('follows the live cart context', () => {
155
155
  useCart.mockReturnValue({ items: [cartItem('pg-1')] })
156
156
 
157
157
  const { rerender } = renderHook(() =>