@shopify/shop-minis-react 0.2.9 → 0.3.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.
- package/dist/components/commerce/add-to-cart.js +70 -53
- package/dist/components/commerce/add-to-cart.js.map +1 -1
- package/dist/components/commerce/buy-now.js +75 -0
- package/dist/components/commerce/buy-now.js.map +1 -0
- package/dist/index.js +230 -230
- package/dist/{hooks/shop → internal}/useShopCartActions.js +2 -2
- package/dist/internal/useShopCartActions.js.map +1 -0
- package/generated-hook-maps/hook-actions-map.json +0 -4
- package/package.json +2 -2
- package/src/components/commerce/add-to-cart.test.tsx +218 -3
- package/src/components/commerce/add-to-cart.tsx +40 -16
- package/src/components/commerce/buy-now.test.tsx +272 -0
- package/src/components/commerce/buy-now.tsx +108 -0
- package/src/components/index.ts +1 -0
- package/src/hooks/index.ts +0 -1
- package/src/{hooks/shop → internal}/useShopCartActions.ts +2 -2
- package/src/stories/AddToCart.stories.tsx +75 -10
- package/dist/hooks/shop/useShopCartActions.js.map +0 -1
|
@@ -1,20 +1,72 @@
|
|
|
1
|
+
import {Product} from '@shopify/shop-minis-platform'
|
|
1
2
|
import {describe, expect, it, vi} from 'vitest'
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
render,
|
|
6
|
+
screen,
|
|
7
|
+
mockMinisSDK,
|
|
8
|
+
resetAllMocks,
|
|
9
|
+
userEvent,
|
|
10
|
+
} from '../../test-utils'
|
|
4
11
|
|
|
5
12
|
import {AddToCartButton} from './add-to-cart'
|
|
6
13
|
|
|
7
14
|
// Mock hooks
|
|
8
|
-
vi.mock('../../
|
|
15
|
+
vi.mock('../../internal/useShopCartActions', () => ({
|
|
9
16
|
useShopCartActions: () => ({
|
|
10
17
|
addToCart: mockMinisSDK.addToCart,
|
|
11
18
|
buyProduct: mockMinisSDK.buyProduct,
|
|
12
19
|
}),
|
|
13
20
|
}))
|
|
14
21
|
|
|
22
|
+
vi.mock('../../hooks', () => ({
|
|
23
|
+
useShopNavigation: () => ({
|
|
24
|
+
navigateToProduct: mockMinisSDK.navigateToProduct,
|
|
25
|
+
}),
|
|
26
|
+
useErrorToast: () => ({
|
|
27
|
+
showErrorToast: vi.fn(),
|
|
28
|
+
}),
|
|
29
|
+
}))
|
|
30
|
+
|
|
15
31
|
describe('AddToCartButton', () => {
|
|
32
|
+
const mockProduct: Product = {
|
|
33
|
+
id: 'gid://shopify/Product/123',
|
|
34
|
+
title: 'Test Product',
|
|
35
|
+
reviewAnalytics: {
|
|
36
|
+
averageRating: null,
|
|
37
|
+
reviewCount: null,
|
|
38
|
+
},
|
|
39
|
+
shop: {
|
|
40
|
+
id: 'gid://shopify/Shop/1',
|
|
41
|
+
name: 'Test Shop',
|
|
42
|
+
},
|
|
43
|
+
defaultVariantId: 'gid://shopify/ProductVariant/456',
|
|
44
|
+
isFavorited: false,
|
|
45
|
+
price: {
|
|
46
|
+
amount: '10.00',
|
|
47
|
+
currencyCode: 'USD',
|
|
48
|
+
},
|
|
49
|
+
variants: [
|
|
50
|
+
{
|
|
51
|
+
id: 'gid://shopify/ProductVariant/456',
|
|
52
|
+
title: 'Default',
|
|
53
|
+
isFavorited: false,
|
|
54
|
+
price: {
|
|
55
|
+
amount: '10.00',
|
|
56
|
+
currencyCode: 'USD',
|
|
57
|
+
},
|
|
58
|
+
image: {
|
|
59
|
+
url: 'https://example.com/variant-image.jpg',
|
|
60
|
+
altText: null,
|
|
61
|
+
width: null,
|
|
62
|
+
height: null,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
}
|
|
67
|
+
|
|
16
68
|
const defaultProps = {
|
|
17
|
-
|
|
69
|
+
product: mockProduct,
|
|
18
70
|
productVariantId: 'gid://shopify/ProductVariant/456',
|
|
19
71
|
}
|
|
20
72
|
|
|
@@ -70,4 +122,167 @@ describe('AddToCartButton', () => {
|
|
|
70
122
|
|
|
71
123
|
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
72
124
|
})
|
|
125
|
+
|
|
126
|
+
it('calls addToCart when clicked and not a referral product', async () => {
|
|
127
|
+
const user = userEvent.setup()
|
|
128
|
+
mockMinisSDK.addToCart.mockResolvedValueOnce({ok: true})
|
|
129
|
+
|
|
130
|
+
render(<AddToCartButton {...defaultProps} />)
|
|
131
|
+
|
|
132
|
+
const button = screen.getByRole('button')
|
|
133
|
+
await user.click(button)
|
|
134
|
+
|
|
135
|
+
expect(mockMinisSDK.addToCart).toHaveBeenCalledWith({
|
|
136
|
+
productId: mockProduct.id,
|
|
137
|
+
productVariantId: defaultProps.productVariantId,
|
|
138
|
+
quantity: 1,
|
|
139
|
+
discountCodes: undefined,
|
|
140
|
+
variantImageUrl: 'https://example.com/variant-image.jpg',
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('navigates to product page when product is referral', async () => {
|
|
145
|
+
const user = userEvent.setup()
|
|
146
|
+
const referralProduct: Product = {...mockProduct, referral: true}
|
|
147
|
+
|
|
148
|
+
render(<AddToCartButton {...defaultProps} product={referralProduct} />)
|
|
149
|
+
|
|
150
|
+
const button = screen.getByRole('button')
|
|
151
|
+
await user.click(button)
|
|
152
|
+
|
|
153
|
+
expect(mockMinisSDK.navigateToProduct).toHaveBeenCalledWith({
|
|
154
|
+
productId: referralProduct.id,
|
|
155
|
+
})
|
|
156
|
+
expect(mockMinisSDK.addToCart).not.toHaveBeenCalled()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('shows success animation after adding to cart', async () => {
|
|
160
|
+
const user = userEvent.setup()
|
|
161
|
+
mockMinisSDK.addToCart.mockResolvedValueOnce({ok: true})
|
|
162
|
+
|
|
163
|
+
render(<AddToCartButton {...defaultProps} />)
|
|
164
|
+
|
|
165
|
+
const button = screen.getByRole('button')
|
|
166
|
+
await user.click(button)
|
|
167
|
+
|
|
168
|
+
// Check for success state (Added to cart text)
|
|
169
|
+
await screen.findByText('Added to cart')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('handles add to cart error gracefully', async () => {
|
|
173
|
+
const user = userEvent.setup()
|
|
174
|
+
mockMinisSDK.addToCart.mockRejectedValueOnce(new Error('Failed'))
|
|
175
|
+
|
|
176
|
+
render(<AddToCartButton {...defaultProps} />)
|
|
177
|
+
|
|
178
|
+
const button = screen.getByRole('button')
|
|
179
|
+
await user.click(button)
|
|
180
|
+
|
|
181
|
+
expect(mockMinisSDK.addToCart).toHaveBeenCalled()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('does not call addToCart when disabled', async () => {
|
|
185
|
+
const user = userEvent.setup()
|
|
186
|
+
|
|
187
|
+
render(<AddToCartButton {...defaultProps} disabled />)
|
|
188
|
+
|
|
189
|
+
const button = screen.getByRole('button')
|
|
190
|
+
await user.click(button)
|
|
191
|
+
|
|
192
|
+
expect(mockMinisSDK.addToCart).not.toHaveBeenCalled()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('handles product without variants array', async () => {
|
|
196
|
+
const user = userEvent.setup()
|
|
197
|
+
mockMinisSDK.addToCart.mockResolvedValueOnce({ok: true})
|
|
198
|
+
|
|
199
|
+
const productWithoutVariants: Product = {
|
|
200
|
+
...mockProduct,
|
|
201
|
+
variants: undefined,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
render(
|
|
205
|
+
<AddToCartButton
|
|
206
|
+
product={productWithoutVariants}
|
|
207
|
+
productVariantId="gid://shopify/ProductVariant/456"
|
|
208
|
+
/>
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
const button = screen.getByRole('button')
|
|
212
|
+
await user.click(button)
|
|
213
|
+
|
|
214
|
+
expect(mockMinisSDK.addToCart).toHaveBeenCalledWith({
|
|
215
|
+
productId: productWithoutVariants.id,
|
|
216
|
+
productVariantId: 'gid://shopify/ProductVariant/456',
|
|
217
|
+
quantity: 1,
|
|
218
|
+
discountCodes: undefined,
|
|
219
|
+
variantImageUrl: undefined,
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('handles product without matching variant in array', async () => {
|
|
224
|
+
const user = userEvent.setup()
|
|
225
|
+
mockMinisSDK.addToCart.mockResolvedValueOnce({ok: true})
|
|
226
|
+
|
|
227
|
+
const productWithDifferentVariant: Product = {
|
|
228
|
+
...mockProduct,
|
|
229
|
+
variants: [
|
|
230
|
+
{
|
|
231
|
+
id: 'gid://shopify/ProductVariant/999',
|
|
232
|
+
title: 'Different',
|
|
233
|
+
isFavorited: false,
|
|
234
|
+
price: {
|
|
235
|
+
amount: '15.00',
|
|
236
|
+
currencyCode: 'USD',
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
render(
|
|
243
|
+
<AddToCartButton
|
|
244
|
+
product={productWithDifferentVariant}
|
|
245
|
+
productVariantId="gid://shopify/ProductVariant/456"
|
|
246
|
+
/>
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
const button = screen.getByRole('button')
|
|
250
|
+
await user.click(button)
|
|
251
|
+
|
|
252
|
+
expect(mockMinisSDK.addToCart).toHaveBeenCalledWith({
|
|
253
|
+
productId: productWithDifferentVariant.id,
|
|
254
|
+
productVariantId: 'gid://shopify/ProductVariant/456',
|
|
255
|
+
quantity: 1,
|
|
256
|
+
discountCodes: undefined,
|
|
257
|
+
variantImageUrl: undefined,
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('handles product without shop data', async () => {
|
|
262
|
+
const user = userEvent.setup()
|
|
263
|
+
mockMinisSDK.addToCart.mockResolvedValueOnce({ok: true})
|
|
264
|
+
|
|
265
|
+
const productWithoutShop: Product = {
|
|
266
|
+
...mockProduct,
|
|
267
|
+
shop: undefined as any,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
render(
|
|
271
|
+
<AddToCartButton
|
|
272
|
+
product={productWithoutShop}
|
|
273
|
+
productVariantId="gid://shopify/ProductVariant/456"
|
|
274
|
+
/>
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
const button = screen.getByRole('button')
|
|
278
|
+
await user.click(button)
|
|
279
|
+
|
|
280
|
+
expect(mockMinisSDK.addToCart).toHaveBeenCalledWith({
|
|
281
|
+
productId: productWithoutShop.id,
|
|
282
|
+
productVariantId: 'gid://shopify/ProductVariant/456',
|
|
283
|
+
quantity: 1,
|
|
284
|
+
discountCodes: undefined,
|
|
285
|
+
variantImageUrl: 'https://example.com/variant-image.jpg',
|
|
286
|
+
})
|
|
287
|
+
})
|
|
73
288
|
})
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import {useState, useCallback} from 'react'
|
|
3
3
|
|
|
4
|
+
import {Product} from '@shopify/shop-minis-platform'
|
|
4
5
|
import {CheckIcon} from 'lucide-react'
|
|
5
6
|
import {motion, AnimatePresence} from 'motion/react'
|
|
6
7
|
|
|
7
|
-
import {useErrorToast,
|
|
8
|
+
import {useErrorToast, useShopNavigation} from '../../hooks'
|
|
9
|
+
import {useShopCartActions} from '../../internal/useShopCartActions'
|
|
8
10
|
import {cn} from '../../lib/utils'
|
|
9
11
|
import {Button} from '../atoms/button'
|
|
10
12
|
|
|
@@ -16,42 +18,58 @@ interface AddToCartButtonProps {
|
|
|
16
18
|
* The discount codes to apply to the cart.
|
|
17
19
|
*/
|
|
18
20
|
discountCodes?: string[]
|
|
19
|
-
/**
|
|
20
|
-
* The GID of the product. E.g. `gid://shopify/Product/123`.
|
|
21
|
-
*/
|
|
22
|
-
productId: string
|
|
23
21
|
/**
|
|
24
22
|
* The GID of the product variant. E.g. `gid://shopify/ProductVariant/456`.
|
|
25
23
|
*/
|
|
26
24
|
productVariantId: string
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The product to add to the cart.
|
|
28
|
+
*/
|
|
29
|
+
product?: Product
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export function AddToCartButton({
|
|
30
33
|
disabled = false,
|
|
31
34
|
className,
|
|
32
35
|
size = 'default',
|
|
33
|
-
productId,
|
|
34
36
|
productVariantId,
|
|
35
37
|
discountCodes,
|
|
38
|
+
product,
|
|
36
39
|
}: AddToCartButtonProps) {
|
|
37
40
|
const {addToCart} = useShopCartActions()
|
|
41
|
+
const {navigateToProduct} = useShopNavigation()
|
|
38
42
|
const [isAdded, setIsAdded] = useState(false)
|
|
39
43
|
const timeoutRef = React.useRef<number | undefined>(undefined)
|
|
44
|
+
const {id, referral, variants} = product ?? {}
|
|
45
|
+
|
|
46
|
+
const variantImageUrl = variants?.find(
|
|
47
|
+
variant => variant.id === productVariantId
|
|
48
|
+
)?.image?.url
|
|
40
49
|
|
|
41
50
|
const {showErrorToast} = useErrorToast()
|
|
42
51
|
|
|
43
52
|
const handleClick = useCallback(async () => {
|
|
44
|
-
if (
|
|
53
|
+
if (disabled) return
|
|
54
|
+
|
|
55
|
+
if (id && referral) {
|
|
56
|
+
navigateToProduct({
|
|
57
|
+
productId: id,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (isAdded) return
|
|
45
64
|
|
|
46
65
|
try {
|
|
47
|
-
|
|
48
|
-
if (productId && productVariantId) {
|
|
49
|
-
// Optimistic update with error toast
|
|
66
|
+
if (id && productVariantId) {
|
|
50
67
|
addToCart({
|
|
51
|
-
productId,
|
|
68
|
+
productId: id,
|
|
52
69
|
productVariantId,
|
|
53
70
|
quantity: 1,
|
|
54
71
|
discountCodes,
|
|
72
|
+
variantImageUrl,
|
|
55
73
|
})
|
|
56
74
|
.then(() => {})
|
|
57
75
|
.catch(() => {
|
|
@@ -77,12 +95,15 @@ export function AddToCartButton({
|
|
|
77
95
|
console.error('Failed to add to cart:', error)
|
|
78
96
|
}
|
|
79
97
|
}, [
|
|
80
|
-
isAdded,
|
|
81
98
|
disabled,
|
|
82
|
-
|
|
83
|
-
|
|
99
|
+
id,
|
|
100
|
+
referral,
|
|
101
|
+
isAdded,
|
|
102
|
+
navigateToProduct,
|
|
84
103
|
productVariantId,
|
|
104
|
+
addToCart,
|
|
85
105
|
discountCodes,
|
|
106
|
+
variantImageUrl,
|
|
86
107
|
showErrorToast,
|
|
87
108
|
])
|
|
88
109
|
|
|
@@ -95,6 +116,9 @@ export function AddToCartButton({
|
|
|
95
116
|
}
|
|
96
117
|
}, [])
|
|
97
118
|
|
|
119
|
+
const addToCartText = isAdded ? 'Added to cart' : 'Add to cart'
|
|
120
|
+
const buttonText = referral ? 'View product' : addToCartText
|
|
121
|
+
|
|
98
122
|
return (
|
|
99
123
|
<Button
|
|
100
124
|
onClick={handleClick}
|
|
@@ -116,7 +140,7 @@ export function AddToCartButton({
|
|
|
116
140
|
duration: 0.4,
|
|
117
141
|
ease: [0.175, 0.885, 0.32, 1.275], // bounce effect
|
|
118
142
|
}}
|
|
119
|
-
className="absolute left-
|
|
143
|
+
className="absolute left-2"
|
|
120
144
|
style={{x: -8}}
|
|
121
145
|
>
|
|
122
146
|
<CheckIcon className="size-4" />
|
|
@@ -124,7 +148,7 @@ export function AddToCartButton({
|
|
|
124
148
|
)}
|
|
125
149
|
</AnimatePresence>
|
|
126
150
|
<span className={cn(isAdded && 'pl-5', 'transition-all duration-300')}>
|
|
127
|
-
{
|
|
151
|
+
{buttonText}
|
|
128
152
|
</span>
|
|
129
153
|
</div>
|
|
130
154
|
</Button>
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import {Product} from '@shopify/shop-minis-platform'
|
|
2
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
render,
|
|
6
|
+
screen,
|
|
7
|
+
mockMinisSDK,
|
|
8
|
+
resetAllMocks,
|
|
9
|
+
userEvent,
|
|
10
|
+
waitFor,
|
|
11
|
+
} from '../../test-utils'
|
|
12
|
+
|
|
13
|
+
import {BuyNowButton} from './buy-now'
|
|
14
|
+
|
|
15
|
+
// Mock hooks
|
|
16
|
+
const mockShowErrorToast = vi.fn()
|
|
17
|
+
|
|
18
|
+
vi.mock('../../internal/useShopCartActions', () => ({
|
|
19
|
+
useShopCartActions: () => ({
|
|
20
|
+
addToCart: mockMinisSDK.addToCart,
|
|
21
|
+
buyProduct: mockMinisSDK.buyProduct,
|
|
22
|
+
}),
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
vi.mock('../../hooks', () => ({
|
|
26
|
+
useShopNavigation: () => ({
|
|
27
|
+
navigateToProduct: mockMinisSDK.navigateToProduct,
|
|
28
|
+
}),
|
|
29
|
+
useErrorToast: () => ({
|
|
30
|
+
showErrorToast: mockShowErrorToast,
|
|
31
|
+
}),
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
describe('BuyNowButton', () => {
|
|
35
|
+
const mockProduct: Product = {
|
|
36
|
+
id: 'gid://shopify/Product/123',
|
|
37
|
+
title: 'Test Product',
|
|
38
|
+
reviewAnalytics: {
|
|
39
|
+
averageRating: null,
|
|
40
|
+
reviewCount: null,
|
|
41
|
+
},
|
|
42
|
+
shop: {
|
|
43
|
+
id: 'gid://shopify/Shop/1',
|
|
44
|
+
name: 'Test Shop',
|
|
45
|
+
},
|
|
46
|
+
defaultVariantId: 'gid://shopify/ProductVariant/456',
|
|
47
|
+
isFavorited: false,
|
|
48
|
+
price: {
|
|
49
|
+
amount: '10.00',
|
|
50
|
+
currencyCode: 'USD',
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const defaultProps = {
|
|
55
|
+
product: mockProduct,
|
|
56
|
+
productVariantId: 'gid://shopify/ProductVariant/456',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// eslint-disable-next-line jest/require-top-level-describe
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
resetAllMocks()
|
|
62
|
+
mockShowErrorToast.mockClear()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('renders with default text', () => {
|
|
66
|
+
render(<BuyNowButton {...defaultProps} />)
|
|
67
|
+
|
|
68
|
+
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
69
|
+
expect(screen.getByText('Buy now')).toBeInTheDocument()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('renders with required props', () => {
|
|
73
|
+
render(<BuyNowButton {...defaultProps} />)
|
|
74
|
+
|
|
75
|
+
const button = screen.getByRole('button')
|
|
76
|
+
expect(button).toBeInTheDocument()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('respects disabled prop', () => {
|
|
80
|
+
render(<BuyNowButton {...defaultProps} disabled />)
|
|
81
|
+
|
|
82
|
+
const button = screen.getByRole('button')
|
|
83
|
+
expect(button).toBeDisabled()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('applies custom className', () => {
|
|
87
|
+
render(<BuyNowButton {...defaultProps} className="custom-class" />)
|
|
88
|
+
|
|
89
|
+
const button = screen.getByRole('button')
|
|
90
|
+
expect(button).toHaveClass('custom-class')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('renders with different sizes', () => {
|
|
94
|
+
const {rerender} = render(<BuyNowButton {...defaultProps} size="sm" />)
|
|
95
|
+
|
|
96
|
+
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
97
|
+
|
|
98
|
+
rerender(<BuyNowButton {...defaultProps} size="default" />)
|
|
99
|
+
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
100
|
+
|
|
101
|
+
rerender(<BuyNowButton {...defaultProps} size="lg" />)
|
|
102
|
+
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('renders with discount code prop', () => {
|
|
106
|
+
const discountCode = 'SUMMER20'
|
|
107
|
+
|
|
108
|
+
render(<BuyNowButton {...defaultProps} discountCode={discountCode} />)
|
|
109
|
+
|
|
110
|
+
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('calls buyProduct when clicked and not a referral product', async () => {
|
|
114
|
+
const user = userEvent.setup()
|
|
115
|
+
mockMinisSDK.buyProduct.mockResolvedValueOnce({ok: true})
|
|
116
|
+
|
|
117
|
+
render(<BuyNowButton {...defaultProps} />)
|
|
118
|
+
|
|
119
|
+
const button = screen.getByRole('button')
|
|
120
|
+
await user.click(button)
|
|
121
|
+
|
|
122
|
+
expect(mockMinisSDK.buyProduct).toHaveBeenCalledWith({
|
|
123
|
+
productId: mockProduct.id,
|
|
124
|
+
productVariantId: defaultProps.productVariantId,
|
|
125
|
+
quantity: 1,
|
|
126
|
+
discountCode: undefined,
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('navigates to product page when product is referral', async () => {
|
|
131
|
+
const user = userEvent.setup()
|
|
132
|
+
const referralProduct: Product = {...mockProduct, referral: true}
|
|
133
|
+
|
|
134
|
+
render(<BuyNowButton {...defaultProps} product={referralProduct} />)
|
|
135
|
+
|
|
136
|
+
// Button should show "View product" instead of "Buy now"
|
|
137
|
+
expect(screen.getByText('View product')).toBeInTheDocument()
|
|
138
|
+
expect(screen.queryByText('Buy now')).not.toBeInTheDocument()
|
|
139
|
+
|
|
140
|
+
const button = screen.getByRole('button')
|
|
141
|
+
await user.click(button)
|
|
142
|
+
|
|
143
|
+
expect(mockMinisSDK.navigateToProduct).toHaveBeenCalledWith({
|
|
144
|
+
productId: referralProduct.id,
|
|
145
|
+
})
|
|
146
|
+
expect(mockMinisSDK.buyProduct).not.toHaveBeenCalled()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('shows processing state while purchasing', async () => {
|
|
150
|
+
const user = userEvent.setup()
|
|
151
|
+
mockMinisSDK.buyProduct.mockImplementation(() => new Promise(() => {})) // Never resolves
|
|
152
|
+
|
|
153
|
+
render(<BuyNowButton {...defaultProps} />)
|
|
154
|
+
|
|
155
|
+
const button = screen.getByRole('button')
|
|
156
|
+
await user.click(button)
|
|
157
|
+
|
|
158
|
+
// Check for processing state - button should be disabled and have aria-busy
|
|
159
|
+
await waitFor(() => {
|
|
160
|
+
expect(button).toBeDisabled()
|
|
161
|
+
expect(button).toHaveAttribute('aria-busy', 'true')
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('handles buy product error gracefully', async () => {
|
|
166
|
+
const user = userEvent.setup()
|
|
167
|
+
const error = new Error('Purchase failed')
|
|
168
|
+
mockMinisSDK.buyProduct.mockRejectedValueOnce(error)
|
|
169
|
+
|
|
170
|
+
render(<BuyNowButton {...defaultProps} />)
|
|
171
|
+
|
|
172
|
+
const button = screen.getByRole('button')
|
|
173
|
+
await user.click(button)
|
|
174
|
+
|
|
175
|
+
await waitFor(() => {
|
|
176
|
+
expect(mockMinisSDK.buyProduct).toHaveBeenCalled()
|
|
177
|
+
expect(mockShowErrorToast).toHaveBeenCalledWith({
|
|
178
|
+
message: 'Failed to complete purchase',
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// Button should be enabled again after error
|
|
183
|
+
expect(button).toBeEnabled()
|
|
184
|
+
expect(button).toHaveAttribute('aria-busy', 'false')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('does not call buyProduct when disabled', async () => {
|
|
188
|
+
const user = userEvent.setup()
|
|
189
|
+
|
|
190
|
+
render(<BuyNowButton {...defaultProps} disabled />)
|
|
191
|
+
|
|
192
|
+
const button = screen.getByRole('button')
|
|
193
|
+
await user.click(button)
|
|
194
|
+
|
|
195
|
+
expect(mockMinisSDK.buyProduct).not.toHaveBeenCalled()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('passes discount code to buyProduct', async () => {
|
|
199
|
+
const user = userEvent.setup()
|
|
200
|
+
const discountCode = 'DISCOUNT10'
|
|
201
|
+
mockMinisSDK.buyProduct.mockResolvedValueOnce({ok: true})
|
|
202
|
+
|
|
203
|
+
render(<BuyNowButton {...defaultProps} discountCode={discountCode} />)
|
|
204
|
+
|
|
205
|
+
const button = screen.getByRole('button')
|
|
206
|
+
await user.click(button)
|
|
207
|
+
|
|
208
|
+
expect(mockMinisSDK.buyProduct).toHaveBeenCalledWith({
|
|
209
|
+
productId: mockProduct.id,
|
|
210
|
+
productVariantId: defaultProps.productVariantId,
|
|
211
|
+
quantity: 1,
|
|
212
|
+
discountCode,
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('handles product without shop data', async () => {
|
|
217
|
+
const user = userEvent.setup()
|
|
218
|
+
mockMinisSDK.buyProduct.mockResolvedValueOnce({ok: true})
|
|
219
|
+
|
|
220
|
+
const productWithoutShop: Product = {
|
|
221
|
+
...mockProduct,
|
|
222
|
+
shop: undefined as any,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
render(
|
|
226
|
+
<BuyNowButton
|
|
227
|
+
product={productWithoutShop}
|
|
228
|
+
productVariantId="gid://shopify/ProductVariant/456"
|
|
229
|
+
/>
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
const button = screen.getByRole('button')
|
|
233
|
+
await user.click(button)
|
|
234
|
+
|
|
235
|
+
expect(mockMinisSDK.buyProduct).toHaveBeenCalledWith({
|
|
236
|
+
productId: productWithoutShop.id,
|
|
237
|
+
productVariantId: 'gid://shopify/ProductVariant/456',
|
|
238
|
+
quantity: 1,
|
|
239
|
+
discountCode: undefined,
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('handles product with partial shop data', async () => {
|
|
244
|
+
const user = userEvent.setup()
|
|
245
|
+
mockMinisSDK.buyProduct.mockResolvedValueOnce({ok: true})
|
|
246
|
+
|
|
247
|
+
const productWithPartialShop: Product = {
|
|
248
|
+
...mockProduct,
|
|
249
|
+
shop: {
|
|
250
|
+
id: 'gid://shopify/Shop/1',
|
|
251
|
+
name: undefined as any,
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
render(
|
|
256
|
+
<BuyNowButton
|
|
257
|
+
product={productWithPartialShop}
|
|
258
|
+
productVariantId="gid://shopify/ProductVariant/456"
|
|
259
|
+
/>
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
const button = screen.getByRole('button')
|
|
263
|
+
await user.click(button)
|
|
264
|
+
|
|
265
|
+
expect(mockMinisSDK.buyProduct).toHaveBeenCalledWith({
|
|
266
|
+
productId: productWithPartialShop.id,
|
|
267
|
+
productVariantId: 'gid://shopify/ProductVariant/456',
|
|
268
|
+
quantity: 1,
|
|
269
|
+
discountCode: undefined,
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
})
|