@shopify/shop-minis-react 0.2.5 → 0.2.6
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/product-card.js +61 -58
- package/dist/components/commerce/product-card.js.map +1 -1
- package/dist/components/commerce/product-link.js +144 -131
- package/dist/components/commerce/product-link.js.map +1 -1
- package/eslint/rules/validate-manifest.cjs +99 -1
- package/package.json +1 -1
- package/src/components/commerce/product-card.test.tsx +68 -0
- package/src/components/commerce/product-card.tsx +10 -2
- package/src/components/commerce/product-link.test.tsx +81 -0
- package/src/components/commerce/product-link.tsx +36 -10
package/package.json
CHANGED
|
@@ -147,6 +147,33 @@ describe('ProductCard', () => {
|
|
|
147
147
|
|
|
148
148
|
expect(screen.getByText('No Image')).toBeInTheDocument()
|
|
149
149
|
})
|
|
150
|
+
|
|
151
|
+
it('renders favorite button by default', () => {
|
|
152
|
+
const product = mockProduct()
|
|
153
|
+
render(<ProductCard product={product} />)
|
|
154
|
+
|
|
155
|
+
// Favorite button should be present
|
|
156
|
+
const favoriteButton = screen.getByRole('button')
|
|
157
|
+
expect(favoriteButton).toBeInTheDocument()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('hides favorite button when favoriteButtonDisabled is true', () => {
|
|
161
|
+
const product = mockProduct()
|
|
162
|
+
render(<ProductCard product={product} favoriteButtonDisabled />)
|
|
163
|
+
|
|
164
|
+
// Favorite button should not be present
|
|
165
|
+
const favoriteButton = screen.queryByRole('button')
|
|
166
|
+
expect(favoriteButton).not.toBeInTheDocument()
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('shows favorite button when favoriteButtonDisabled is false', () => {
|
|
170
|
+
const product = mockProduct()
|
|
171
|
+
render(<ProductCard product={product} favoriteButtonDisabled={false} />)
|
|
172
|
+
|
|
173
|
+
// Favorite button should be present
|
|
174
|
+
const favoriteButton = screen.getByRole('button')
|
|
175
|
+
expect(favoriteButton).toBeInTheDocument()
|
|
176
|
+
})
|
|
150
177
|
})
|
|
151
178
|
|
|
152
179
|
describe('Interactions', () => {
|
|
@@ -262,6 +289,28 @@ describe('ProductCard', () => {
|
|
|
262
289
|
expect(favoriteButton).toHaveClass('bg-button-overlay/30')
|
|
263
290
|
})
|
|
264
291
|
})
|
|
292
|
+
|
|
293
|
+
it('does not allow favorite toggle when favoriteButtonDisabled is true', async () => {
|
|
294
|
+
const product = mockProduct({isFavorited: false})
|
|
295
|
+
const onFavoriteToggled = vi.fn()
|
|
296
|
+
|
|
297
|
+
render(
|
|
298
|
+
<ProductCard
|
|
299
|
+
product={product}
|
|
300
|
+
favoriteButtonDisabled
|
|
301
|
+
onFavoriteToggled={onFavoriteToggled}
|
|
302
|
+
/>
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
// Button should not exist at all
|
|
306
|
+
const favoriteButton = screen.queryByRole('button')
|
|
307
|
+
expect(favoriteButton).not.toBeInTheDocument()
|
|
308
|
+
|
|
309
|
+
// Callbacks should not be called
|
|
310
|
+
expect(onFavoriteToggled).not.toHaveBeenCalled()
|
|
311
|
+
expect(mockMinisSDK.saveProduct).not.toHaveBeenCalled()
|
|
312
|
+
expect(mockMinisSDK.unsaveProduct).not.toHaveBeenCalled()
|
|
313
|
+
})
|
|
265
314
|
})
|
|
266
315
|
|
|
267
316
|
describe('Custom Composition', () => {
|
|
@@ -307,6 +356,25 @@ describe('ProductCard', () => {
|
|
|
307
356
|
expect(screen.getByText(product.title)).toBeInTheDocument()
|
|
308
357
|
expect(screen.getByText('$99.99')).toBeInTheDocument()
|
|
309
358
|
})
|
|
359
|
+
|
|
360
|
+
it('respects favoriteButtonDisabled in custom composition', () => {
|
|
361
|
+
const product = mockProduct()
|
|
362
|
+
|
|
363
|
+
render(
|
|
364
|
+
<ProductCard product={product} favoriteButtonDisabled>
|
|
365
|
+
<ProductCardContainer>
|
|
366
|
+
<ProductCardImageContainer>
|
|
367
|
+
<ProductCardImage />
|
|
368
|
+
<ProductCardFavoriteButton />
|
|
369
|
+
</ProductCardImageContainer>
|
|
370
|
+
</ProductCardContainer>
|
|
371
|
+
</ProductCard>
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
// Favorite button should not be rendered even when explicitly included
|
|
375
|
+
const favoriteButton = screen.queryByRole('button')
|
|
376
|
+
expect(favoriteButton).not.toBeInTheDocument()
|
|
377
|
+
})
|
|
310
378
|
})
|
|
311
379
|
|
|
312
380
|
describe('Edge Cases', () => {
|
|
@@ -28,6 +28,7 @@ interface ProductCardContextValue {
|
|
|
28
28
|
|
|
29
29
|
// State
|
|
30
30
|
isFavorited: boolean
|
|
31
|
+
isFavoriteButtonDisabled: boolean
|
|
31
32
|
|
|
32
33
|
// Actions
|
|
33
34
|
onClick: () => void
|
|
@@ -194,7 +195,10 @@ function ProductCardFavoriteButton({
|
|
|
194
195
|
className,
|
|
195
196
|
...props
|
|
196
197
|
}: React.ComponentProps<'div'>) {
|
|
197
|
-
const {isFavorited, onFavoriteToggle} =
|
|
198
|
+
const {isFavorited, isFavoriteButtonDisabled, onFavoriteToggle} =
|
|
199
|
+
useProductCardContext()
|
|
200
|
+
if (isFavoriteButtonDisabled) return null
|
|
201
|
+
|
|
198
202
|
return (
|
|
199
203
|
<div className={cn('absolute bottom-3 right-3 z-10', className)} {...props}>
|
|
200
204
|
<FavoriteButton onClick={onFavoriteToggle} filled={isFavorited} />
|
|
@@ -292,6 +296,8 @@ export interface ProductCardProps {
|
|
|
292
296
|
onFavoriteToggled?: (isFavorited: boolean) => void
|
|
293
297
|
/** Custom layout via children */
|
|
294
298
|
children?: React.ReactNode
|
|
299
|
+
/** Whether the favorite button is disabled */
|
|
300
|
+
favoriteButtonDisabled?: boolean
|
|
295
301
|
}
|
|
296
302
|
|
|
297
303
|
function ProductCard({
|
|
@@ -304,6 +310,7 @@ function ProductCard({
|
|
|
304
310
|
onProductClick,
|
|
305
311
|
onFavoriteToggled,
|
|
306
312
|
children,
|
|
313
|
+
favoriteButtonDisabled = false,
|
|
307
314
|
}: ProductCardProps) {
|
|
308
315
|
const {navigateToProduct} = useShopNavigation()
|
|
309
316
|
const {saveProduct, unsaveProduct} = useSavedProductsActions()
|
|
@@ -374,7 +381,7 @@ function ProductCard({
|
|
|
374
381
|
|
|
375
382
|
// State
|
|
376
383
|
isFavorited: isFavoritedLocal,
|
|
377
|
-
|
|
384
|
+
isFavoriteButtonDisabled: favoriteButtonDisabled,
|
|
378
385
|
// Actions
|
|
379
386
|
onClick: handleClick,
|
|
380
387
|
onFavoriteToggle: handleFavoriteClick,
|
|
@@ -389,6 +396,7 @@ function ProductCard({
|
|
|
389
396
|
isFavoritedLocal,
|
|
390
397
|
handleClick,
|
|
391
398
|
handleFavoriteClick,
|
|
399
|
+
favoriteButtonDisabled,
|
|
392
400
|
]
|
|
393
401
|
)
|
|
394
402
|
|
|
@@ -184,6 +184,53 @@ describe('ProductLink', () => {
|
|
|
184
184
|
// Should not have favorite button
|
|
185
185
|
expect(screen.queryByRole('button')).not.toBeInTheDocument()
|
|
186
186
|
})
|
|
187
|
+
|
|
188
|
+
it('renders custom action when provided', () => {
|
|
189
|
+
const product = mockProduct()
|
|
190
|
+
const onCustomActionClick = vi.fn()
|
|
191
|
+
render(
|
|
192
|
+
<ProductLink
|
|
193
|
+
product={product}
|
|
194
|
+
customAction={
|
|
195
|
+
<button type="button" data-testid="custom-cta">
|
|
196
|
+
Add to Cart
|
|
197
|
+
</button>
|
|
198
|
+
}
|
|
199
|
+
onCustomActionClick={onCustomActionClick}
|
|
200
|
+
/>
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
// Custom action should be rendered
|
|
204
|
+
expect(screen.getByTestId('custom-cta')).toBeInTheDocument()
|
|
205
|
+
expect(screen.getByText('Add to Cart')).toBeInTheDocument()
|
|
206
|
+
|
|
207
|
+
// Favorite button should not be rendered when custom action is present
|
|
208
|
+
// Check that there's only one button (the custom one)
|
|
209
|
+
const buttons = screen.getAllByRole('button')
|
|
210
|
+
expect(buttons).toHaveLength(1)
|
|
211
|
+
expect(buttons[0]).toHaveAttribute('data-testid', 'custom-cta')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('renders custom action even when hideFavoriteAction is true', () => {
|
|
215
|
+
const product = mockProduct()
|
|
216
|
+
const onCustomActionClick = vi.fn()
|
|
217
|
+
render(
|
|
218
|
+
<ProductLink
|
|
219
|
+
product={product}
|
|
220
|
+
hideFavoriteAction
|
|
221
|
+
customAction={
|
|
222
|
+
<button type="button" data-testid="custom-action">
|
|
223
|
+
Buy Now
|
|
224
|
+
</button>
|
|
225
|
+
}
|
|
226
|
+
onCustomActionClick={onCustomActionClick}
|
|
227
|
+
/>
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
// Custom action should still be rendered
|
|
231
|
+
expect(screen.getByTestId('custom-action')).toBeInTheDocument()
|
|
232
|
+
expect(screen.getByText('Buy Now')).toBeInTheDocument()
|
|
233
|
+
})
|
|
187
234
|
})
|
|
188
235
|
|
|
189
236
|
describe('Interactions', () => {
|
|
@@ -313,6 +360,40 @@ describe('ProductLink', () => {
|
|
|
313
360
|
expect(onClick).not.toHaveBeenCalled()
|
|
314
361
|
expect(mockMinisSDK.navigateToProduct).not.toHaveBeenCalled()
|
|
315
362
|
})
|
|
363
|
+
|
|
364
|
+
it('calls onCustomActionClick when custom action is clicked', async () => {
|
|
365
|
+
const user = userEvent.setup()
|
|
366
|
+
const product = mockProduct()
|
|
367
|
+
const onClick = vi.fn()
|
|
368
|
+
const onCustomActionClick = vi.fn()
|
|
369
|
+
|
|
370
|
+
render(
|
|
371
|
+
<ProductLink
|
|
372
|
+
product={product}
|
|
373
|
+
onClick={onClick}
|
|
374
|
+
customAction={
|
|
375
|
+
<button type="button" data-testid="custom-btn">
|
|
376
|
+
Quick Add
|
|
377
|
+
</button>
|
|
378
|
+
}
|
|
379
|
+
onCustomActionClick={onCustomActionClick}
|
|
380
|
+
/>
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
const customButton = screen.getByTestId('custom-btn')
|
|
384
|
+
await user.click(customButton)
|
|
385
|
+
|
|
386
|
+
// Should call custom action handler
|
|
387
|
+
expect(onCustomActionClick).toHaveBeenCalledTimes(1)
|
|
388
|
+
|
|
389
|
+
// Should NOT call favorite save/unsave
|
|
390
|
+
expect(mockMinisSDK.saveProduct).not.toHaveBeenCalled()
|
|
391
|
+
expect(mockMinisSDK.unsaveProduct).not.toHaveBeenCalled()
|
|
392
|
+
|
|
393
|
+
// Should NOT trigger product navigation
|
|
394
|
+
expect(onClick).not.toHaveBeenCalled()
|
|
395
|
+
expect(mockMinisSDK.navigateToProduct).not.toHaveBeenCalled()
|
|
396
|
+
})
|
|
316
397
|
})
|
|
317
398
|
|
|
318
399
|
describe('Star Rating Display', () => {
|
|
@@ -194,13 +194,21 @@ function ProductLinkRating({className, ...props}: React.ComponentProps<'div'>) {
|
|
|
194
194
|
|
|
195
195
|
function ProductLinkActions({
|
|
196
196
|
className,
|
|
197
|
+
hideFavoriteAction = false,
|
|
197
198
|
onPress,
|
|
198
199
|
filled = false,
|
|
200
|
+
customAction,
|
|
199
201
|
...props
|
|
200
202
|
}: React.ComponentProps<typeof CardAction> & {
|
|
203
|
+
hideFavoriteAction?: boolean
|
|
201
204
|
onPress?: () => void
|
|
202
205
|
filled?: boolean
|
|
206
|
+
customAction?: React.ReactNode
|
|
203
207
|
}) {
|
|
208
|
+
const favoriteAction = hideFavoriteAction ? null : (
|
|
209
|
+
<FavoriteButton filled={filled} onClick={onPress} />
|
|
210
|
+
)
|
|
211
|
+
|
|
204
212
|
return (
|
|
205
213
|
<CardAction
|
|
206
214
|
className={cn('flex-shrink-0 self-center px-0 py-0', className)}
|
|
@@ -215,23 +223,34 @@ function ProductLinkActions({
|
|
|
215
223
|
scale: {type: 'tween', duration: 0.08, ease: 'easeInOut'},
|
|
216
224
|
}}
|
|
217
225
|
>
|
|
218
|
-
|
|
226
|
+
{customAction ? <>{customAction}</> : favoriteAction}
|
|
219
227
|
</Touchable>
|
|
220
228
|
</CardAction>
|
|
221
229
|
)
|
|
222
230
|
}
|
|
223
231
|
|
|
224
|
-
export
|
|
232
|
+
export type ProductLinkProps = {
|
|
225
233
|
product: Product
|
|
226
234
|
hideFavoriteAction?: boolean
|
|
227
235
|
onClick?: (product: Product) => void
|
|
228
|
-
}
|
|
236
|
+
} & (
|
|
237
|
+
| {
|
|
238
|
+
customAction?: never
|
|
239
|
+
onCustomActionClick?: never
|
|
240
|
+
}
|
|
241
|
+
| {
|
|
242
|
+
customAction: React.ReactNode
|
|
243
|
+
onCustomActionClick: () => void
|
|
244
|
+
}
|
|
245
|
+
)
|
|
229
246
|
|
|
230
247
|
// Composed ProductLink component
|
|
231
248
|
function ProductLink({
|
|
232
249
|
product,
|
|
233
250
|
hideFavoriteAction = false,
|
|
234
251
|
onClick,
|
|
252
|
+
customAction,
|
|
253
|
+
onCustomActionClick,
|
|
235
254
|
}: ProductLinkProps) {
|
|
236
255
|
const {navigateToProduct} = useShopNavigation()
|
|
237
256
|
const {saveProduct, unsaveProduct} = useSavedProductsActions()
|
|
@@ -272,6 +291,11 @@ function ProductLink({
|
|
|
272
291
|
}, [navigateToProduct, id, onClick, product])
|
|
273
292
|
|
|
274
293
|
const handleActionPress = React.useCallback(async () => {
|
|
294
|
+
if (customAction || onCustomActionClick) {
|
|
295
|
+
onCustomActionClick?.()
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
275
299
|
const previousState = isFavoritedLocal
|
|
276
300
|
|
|
277
301
|
// Optimistic update
|
|
@@ -296,13 +320,15 @@ function ProductLink({
|
|
|
296
320
|
setIsFavoritedLocal(previousState)
|
|
297
321
|
}
|
|
298
322
|
}, [
|
|
323
|
+
customAction,
|
|
324
|
+
onCustomActionClick,
|
|
299
325
|
isFavoritedLocal,
|
|
326
|
+
unsaveProduct,
|
|
300
327
|
id,
|
|
301
328
|
shop.id,
|
|
302
329
|
selectedVariant?.id,
|
|
303
330
|
defaultVariantId,
|
|
304
331
|
saveProduct,
|
|
305
|
-
unsaveProduct,
|
|
306
332
|
])
|
|
307
333
|
|
|
308
334
|
return (
|
|
@@ -366,12 +392,12 @@ function ProductLink({
|
|
|
366
392
|
</ProductLinkPrice>
|
|
367
393
|
</ProductLinkInfo>
|
|
368
394
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
395
|
+
<ProductLinkActions
|
|
396
|
+
filled={isFavoritedLocal}
|
|
397
|
+
onPress={handleActionPress}
|
|
398
|
+
customAction={customAction}
|
|
399
|
+
hideFavoriteAction={hideFavoriteAction}
|
|
400
|
+
/>
|
|
375
401
|
</ProductLinkRoot>
|
|
376
402
|
)
|
|
377
403
|
}
|