@shopify/shop-minis-react 0.0.34 → 0.0.35

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 (64) hide show
  1. package/dist/_virtual/index4.js +2 -2
  2. package/dist/_virtual/index5.js +2 -3
  3. package/dist/_virtual/index5.js.map +1 -1
  4. package/dist/_virtual/index6.js +2 -2
  5. package/dist/_virtual/index7.js +3 -2
  6. package/dist/_virtual/index7.js.map +1 -1
  7. package/dist/components/atoms/alert-dialog.js.map +1 -1
  8. package/dist/components/atoms/icon-button.js +12 -12
  9. package/dist/components/atoms/icon-button.js.map +1 -1
  10. package/dist/components/atoms/text-input.js +22 -0
  11. package/dist/components/atoms/text-input.js.map +1 -0
  12. package/dist/components/commerce/merchant-card.js +1 -0
  13. package/dist/components/commerce/merchant-card.js.map +1 -1
  14. package/dist/components/ui/input.js +15 -9
  15. package/dist/components/ui/input.js.map +1 -1
  16. package/dist/hooks/util/useKeyboardAvoidingView.js +23 -0
  17. package/dist/hooks/util/useKeyboardAvoidingView.js.map +1 -0
  18. package/dist/index.js +226 -222
  19. package/dist/index.js.map +1 -1
  20. package/dist/mocks.js +4 -1
  21. package/dist/mocks.js.map +1 -1
  22. package/dist/shop-minis-react/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.1.6_react@19.1.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.js +1 -1
  23. package/dist/shop-minis-react/node_modules/.pnpm/@videojs_xhr@2.7.0/node_modules/@videojs/xhr/lib/index.js +1 -1
  24. package/dist/shop-minis-react/node_modules/.pnpm/mpd-parser@1.3.1/node_modules/mpd-parser/dist/mpd-parser.es.js +1 -1
  25. package/dist/shop-minis-react/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.js +1 -1
  26. package/dist/utils/image.js +5 -4
  27. package/dist/utils/image.js.map +1 -1
  28. package/package.json +21 -4
  29. package/src/components/atoms/alert-dialog.test.tsx +67 -0
  30. package/src/components/atoms/alert-dialog.tsx +13 -11
  31. package/src/components/atoms/favorite-button.test.tsx +56 -0
  32. package/src/components/atoms/icon-button.tsx +1 -1
  33. package/src/components/atoms/image.test.tsx +108 -0
  34. package/src/components/atoms/product-variant-price.test.tsx +128 -0
  35. package/src/components/atoms/text-input.test.tsx +104 -0
  36. package/src/components/atoms/text-input.tsx +31 -0
  37. package/src/components/commerce/merchant-card.test.tsx +261 -0
  38. package/src/components/commerce/merchant-card.tsx +2 -0
  39. package/src/components/commerce/product-card.test.tsx +364 -0
  40. package/src/components/commerce/product-link.test.tsx +483 -0
  41. package/src/components/commerce/quantity-selector.test.tsx +382 -0
  42. package/src/components/commerce/search.test.tsx +487 -0
  43. package/src/components/content/image-content-wrapper.test.tsx +92 -0
  44. package/src/components/index.ts +1 -0
  45. package/src/components/navigation/transition-link.test.tsx +155 -0
  46. package/src/components/ui/input.test.tsx +21 -0
  47. package/src/components/ui/input.tsx +10 -1
  48. package/src/hooks/content/useCreateImageContent.test.ts +352 -0
  49. package/src/hooks/index.ts +1 -0
  50. package/src/hooks/navigation/useNavigateWithTransition.test.ts +371 -0
  51. package/src/hooks/navigation/useViewTransitions.test.ts +469 -0
  52. package/src/hooks/product/useProductSearch.test.ts +470 -0
  53. package/src/hooks/storage/useAsyncStorage.test.ts +225 -0
  54. package/src/hooks/storage/useImageUpload.test.ts +322 -0
  55. package/src/hooks/util/useKeyboardAvoidingView.ts +37 -0
  56. package/src/internal/useHandleAction.test.ts +265 -0
  57. package/src/internal/useShopActionsDataFetching.test.ts +465 -0
  58. package/src/mocks.ts +3 -1
  59. package/src/providers/ImagePickerProvider.test.tsx +467 -0
  60. package/src/stories/ProductCard.stories.tsx +2 -2
  61. package/src/stories/TextInput.stories.tsx +26 -0
  62. package/src/test-setup.ts +34 -0
  63. package/src/test-utils.tsx +167 -0
  64. package/src/utils/image.ts +1 -0
@@ -0,0 +1,364 @@
1
+ import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest'
2
+
3
+ import {
4
+ render,
5
+ screen,
6
+ userEvent,
7
+ waitFor,
8
+ mockProduct,
9
+ mockProductVariant,
10
+ mockMinisSDK,
11
+ resetAllMocks,
12
+ } from '../../test-utils'
13
+
14
+ import {
15
+ ProductCard,
16
+ ProductCardContainer,
17
+ ProductCardImageContainer,
18
+ ProductCardImage,
19
+ ProductCardBadge,
20
+ ProductCardFavoriteButton,
21
+ ProductCardInfo,
22
+ ProductCardTitle,
23
+ ProductCardPrice,
24
+ } from './product-card'
25
+
26
+ // Mock hooks
27
+ vi.mock('../../hooks/navigation/useShopNavigation', () => ({
28
+ useShopNavigation: () => ({
29
+ navigateToProduct: mockMinisSDK.navigateToProduct,
30
+ navigateToShop: mockMinisSDK.navigateToShop,
31
+ }),
32
+ }))
33
+
34
+ vi.mock('../../hooks/user/useSavedProductsActions', () => ({
35
+ useSavedProductsActions: () => ({
36
+ saveProduct: mockMinisSDK.saveProduct,
37
+ unsaveProduct: mockMinisSDK.unsaveProduct,
38
+ }),
39
+ }))
40
+
41
+ // Mock formatMoney
42
+ vi.mock('../../lib/formatMoney', () => ({
43
+ formatMoney: vi.fn((amount: string, currencyCode: string) => {
44
+ const numAmount = parseFloat(amount)
45
+ return currencyCode === 'USD'
46
+ ? `$${numAmount.toFixed(2)}`
47
+ : `${currencyCode} ${numAmount.toFixed(2)}`
48
+ }),
49
+ }))
50
+
51
+ describe('ProductCard', () => {
52
+ beforeEach(() => {
53
+ resetAllMocks()
54
+ })
55
+
56
+ afterEach(() => {
57
+ vi.clearAllMocks()
58
+ })
59
+
60
+ describe('Rendering', () => {
61
+ it('renders with default variant', () => {
62
+ const product = mockProduct()
63
+ render(<ProductCard product={product} />)
64
+
65
+ // Check for product title
66
+ expect(screen.getByText(product.title)).toBeInTheDocument()
67
+
68
+ // Check for price (default from createProduct is $99.99)
69
+ expect(screen.getByText('$99.99')).toBeInTheDocument()
70
+
71
+ // Check for image container
72
+ expect(screen.getByRole('img')).toBeInTheDocument()
73
+ })
74
+
75
+ it('renders with priceOverlay variant', () => {
76
+ const product = mockProduct()
77
+ render(<ProductCard product={product} variant="priceOverlay" />)
78
+
79
+ // Price should be in a badge on top-left
80
+ const badges = screen.getAllByText('$99.99')
81
+ expect(badges.length).toBeGreaterThan(0)
82
+
83
+ // Should not show product info section in priceOverlay variant
84
+ expect(screen.queryByText(product.title)).toBeNull()
85
+ })
86
+
87
+ it('renders with compact variant', () => {
88
+ const product = mockProduct()
89
+ render(<ProductCard product={product} variant="compact" />)
90
+
91
+ // Should not show product info section in compact variant
92
+ const title = screen.queryByText(product.title)
93
+ expect(title).toBeNull()
94
+ })
95
+
96
+ it('renders with selected variant', () => {
97
+ const product = mockProduct()
98
+ const selectedVariant = mockProductVariant({
99
+ id: 'variant-2',
100
+ price: {amount: '39.99', currencyCode: 'USD'},
101
+ })
102
+
103
+ render(
104
+ <ProductCard
105
+ product={product}
106
+ selectedProductVariant={selectedVariant}
107
+ />
108
+ )
109
+
110
+ // Should show selected variant price
111
+ expect(screen.getByText('$39.99')).toBeInTheDocument()
112
+ })
113
+
114
+ it('renders with compare at price', () => {
115
+ const product = mockProduct({
116
+ price: {amount: '29.99', currencyCode: 'USD'},
117
+ compareAtPrice: {amount: '39.99', currencyCode: 'USD'},
118
+ })
119
+
120
+ render(<ProductCard product={product} />)
121
+
122
+ // Should show both prices
123
+ expect(screen.getByText('$29.99')).toBeInTheDocument()
124
+ expect(screen.getByText('$39.99')).toBeInTheDocument()
125
+
126
+ // Compare at price should have line-through
127
+ const compareAtPrice = screen.getByText('$39.99')
128
+ expect(compareAtPrice).toHaveClass('line-through')
129
+ })
130
+
131
+ it('renders badge when provided', () => {
132
+ const product = mockProduct()
133
+ render(
134
+ <ProductCard
135
+ product={product}
136
+ badgeText="Sale"
137
+ badgeVariant="destructive"
138
+ />
139
+ )
140
+
141
+ expect(screen.getByText('Sale')).toBeInTheDocument()
142
+ })
143
+
144
+ it('renders without image when product has no featured image', () => {
145
+ const product = mockProduct({featuredImage: undefined})
146
+ render(<ProductCard product={product} />)
147
+
148
+ expect(screen.getByText('No Image')).toBeInTheDocument()
149
+ })
150
+ })
151
+
152
+ describe('Interactions', () => {
153
+ it('handles product click navigation', async () => {
154
+ const user = userEvent.setup()
155
+ const product = mockProduct()
156
+ const onProductClick = vi.fn()
157
+
158
+ render(<ProductCard product={product} onProductClick={onProductClick} />)
159
+
160
+ const container = screen.getByText(product.title).closest('div')
161
+ ?.parentElement?.parentElement
162
+ await user.click(container!)
163
+
164
+ expect(onProductClick).toHaveBeenCalledTimes(1)
165
+ expect(mockMinisSDK.navigateToProduct).toHaveBeenCalledWith({
166
+ productId: product.id,
167
+ })
168
+ })
169
+
170
+ it('does not navigate when touchable is false', async () => {
171
+ const user = userEvent.setup()
172
+ const product = mockProduct()
173
+ const onProductClick = vi.fn()
174
+
175
+ render(
176
+ <ProductCard
177
+ product={product}
178
+ touchable={false}
179
+ onProductClick={onProductClick}
180
+ />
181
+ )
182
+
183
+ const container = screen.getByText(product.title).closest('div')
184
+ ?.parentElement?.parentElement
185
+ await user.click(container!)
186
+
187
+ expect(onProductClick).not.toHaveBeenCalled()
188
+ expect(mockMinisSDK.navigateToProduct).not.toHaveBeenCalled()
189
+ })
190
+
191
+ it('handles favorite toggle - save product', async () => {
192
+ const user = userEvent.setup()
193
+ const product = mockProduct({isFavorited: false})
194
+ const onFavoriteToggled = vi.fn()
195
+ mockMinisSDK.saveProduct.mockResolvedValue(undefined)
196
+
197
+ render(
198
+ <ProductCard product={product} onFavoriteToggled={onFavoriteToggled} />
199
+ )
200
+
201
+ const favoriteButton = screen.getByRole('button')
202
+ await user.click(favoriteButton)
203
+
204
+ await waitFor(() => {
205
+ expect(mockMinisSDK.saveProduct).toHaveBeenCalledWith({
206
+ productId: product.id,
207
+ shopId: product.shop.id,
208
+ productVariantId: product.defaultVariantId,
209
+ })
210
+ expect(onFavoriteToggled).toHaveBeenCalledWith(true)
211
+ })
212
+ })
213
+
214
+ it('handles favorite toggle - unsave product', async () => {
215
+ const user = userEvent.setup()
216
+ const product = mockProduct({isFavorited: true})
217
+ const onFavoriteToggled = vi.fn()
218
+ mockMinisSDK.unsaveProduct.mockResolvedValue(undefined)
219
+
220
+ render(
221
+ <ProductCard product={product} onFavoriteToggled={onFavoriteToggled} />
222
+ )
223
+
224
+ const favoriteButton = screen.getByRole('button')
225
+ await user.click(favoriteButton)
226
+
227
+ await waitFor(() => {
228
+ expect(mockMinisSDK.unsaveProduct).toHaveBeenCalledWith({
229
+ productId: product.id,
230
+ shopId: product.shop.id,
231
+ productVariantId: product.defaultVariantId,
232
+ })
233
+ expect(onFavoriteToggled).toHaveBeenCalledWith(false)
234
+ })
235
+ })
236
+
237
+ it('reverts favorite state on error', async () => {
238
+ const user = userEvent.setup()
239
+ const product = mockProduct({isFavorited: false})
240
+ const onFavoriteToggled = vi.fn()
241
+
242
+ // Mock save to reject
243
+ mockMinisSDK.saveProduct.mockRejectedValue(new Error('Save failed'))
244
+
245
+ render(
246
+ <ProductCard product={product} onFavoriteToggled={onFavoriteToggled} />
247
+ )
248
+
249
+ const favoriteButton = screen.getByRole('button')
250
+
251
+ // Initially unfilled
252
+ expect(favoriteButton).toHaveClass('bg-button-overlay/30')
253
+
254
+ await user.click(favoriteButton)
255
+
256
+ // Should call save
257
+ expect(mockMinisSDK.saveProduct).toHaveBeenCalled()
258
+
259
+ // Wait for error handling
260
+ await waitFor(() => {
261
+ // Should revert to unfilled state
262
+ expect(favoriteButton).toHaveClass('bg-button-overlay/30')
263
+ })
264
+ })
265
+ })
266
+
267
+ describe('Custom Composition', () => {
268
+ it('renders with custom children layout', () => {
269
+ const product = mockProduct()
270
+
271
+ render(
272
+ <ProductCard product={product}>
273
+ <ProductCardContainer>
274
+ <div data-testid="custom-layout">
275
+ <ProductCardTitle>Custom Title</ProductCardTitle>
276
+ <ProductCardPrice />
277
+ </div>
278
+ </ProductCardContainer>
279
+ </ProductCard>
280
+ )
281
+
282
+ expect(screen.getByTestId('custom-layout')).toBeInTheDocument()
283
+ expect(screen.getByText('Custom Title')).toBeInTheDocument()
284
+ expect(screen.getByText('$99.99')).toBeInTheDocument()
285
+ })
286
+
287
+ it('allows individual component usage', () => {
288
+ const product = mockProduct()
289
+
290
+ render(
291
+ <ProductCard product={product}>
292
+ <ProductCardContainer>
293
+ <ProductCardImageContainer>
294
+ <ProductCardImage />
295
+ <ProductCardBadge>Custom Badge</ProductCardBadge>
296
+ <ProductCardFavoriteButton />
297
+ </ProductCardImageContainer>
298
+ <ProductCardInfo>
299
+ <ProductCardTitle />
300
+ <ProductCardPrice />
301
+ </ProductCardInfo>
302
+ </ProductCardContainer>
303
+ </ProductCard>
304
+ )
305
+
306
+ expect(screen.getByText('Custom Badge')).toBeInTheDocument()
307
+ expect(screen.getByText(product.title)).toBeInTheDocument()
308
+ expect(screen.getByText('$99.99')).toBeInTheDocument()
309
+ })
310
+ })
311
+
312
+ describe('Edge Cases', () => {
313
+ it('handles product without variants', () => {
314
+ const product = mockProduct({
315
+ variants: [],
316
+ selectedVariant: undefined,
317
+ })
318
+
319
+ render(<ProductCard product={product} />)
320
+
321
+ expect(screen.getByText(product.title)).toBeInTheDocument()
322
+ expect(screen.getByText('$99.99')).toBeInTheDocument()
323
+ })
324
+ })
325
+
326
+ describe('Accessibility', () => {
327
+ it('has proper alt text for images', () => {
328
+ const product = mockProduct()
329
+ render(<ProductCard product={product} />)
330
+
331
+ const img = screen.getByRole('img')
332
+ expect(img).toHaveAttribute(
333
+ 'alt',
334
+ product.featuredImage?.altText || product.title
335
+ )
336
+ })
337
+
338
+ it('uses product title as alt when image alt text is missing', () => {
339
+ const product = mockProduct({
340
+ featuredImage: {
341
+ url: 'https://example.com/image.jpg',
342
+ altText: '',
343
+ width: 1000,
344
+ height: 1000,
345
+ sensitive: false,
346
+ thumbhash: 'thumbhash',
347
+ },
348
+ })
349
+
350
+ render(<ProductCard product={product} />)
351
+
352
+ const img = screen.getByRole('img')
353
+ expect(img).toHaveAttribute('alt', product.title)
354
+ })
355
+
356
+ it('maintains proper heading hierarchy', () => {
357
+ const product = mockProduct()
358
+ render(<ProductCard product={product} />)
359
+
360
+ const heading = screen.getByRole('heading', {level: 3})
361
+ expect(heading).toHaveTextContent(product.title)
362
+ })
363
+ })
364
+ })