@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.
- package/dist/_virtual/index4.js +2 -2
- package/dist/_virtual/index5.js +2 -3
- package/dist/_virtual/index5.js.map +1 -1
- package/dist/_virtual/index6.js +2 -2
- package/dist/_virtual/index7.js +3 -2
- package/dist/_virtual/index7.js.map +1 -1
- package/dist/components/atoms/alert-dialog.js.map +1 -1
- package/dist/components/atoms/icon-button.js +12 -12
- package/dist/components/atoms/icon-button.js.map +1 -1
- package/dist/components/atoms/text-input.js +22 -0
- package/dist/components/atoms/text-input.js.map +1 -0
- package/dist/components/commerce/merchant-card.js +1 -0
- package/dist/components/commerce/merchant-card.js.map +1 -1
- package/dist/components/ui/input.js +15 -9
- package/dist/components/ui/input.js.map +1 -1
- package/dist/hooks/util/useKeyboardAvoidingView.js +23 -0
- package/dist/hooks/util/useKeyboardAvoidingView.js.map +1 -0
- package/dist/index.js +226 -222
- package/dist/index.js.map +1 -1
- package/dist/mocks.js +4 -1
- package/dist/mocks.js.map +1 -1
- 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
- package/dist/shop-minis-react/node_modules/.pnpm/@videojs_xhr@2.7.0/node_modules/@videojs/xhr/lib/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/mpd-parser@1.3.1/node_modules/mpd-parser/dist/mpd-parser.es.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.js +1 -1
- package/dist/utils/image.js +5 -4
- package/dist/utils/image.js.map +1 -1
- package/package.json +21 -4
- package/src/components/atoms/alert-dialog.test.tsx +67 -0
- package/src/components/atoms/alert-dialog.tsx +13 -11
- package/src/components/atoms/favorite-button.test.tsx +56 -0
- package/src/components/atoms/icon-button.tsx +1 -1
- package/src/components/atoms/image.test.tsx +108 -0
- package/src/components/atoms/product-variant-price.test.tsx +128 -0
- package/src/components/atoms/text-input.test.tsx +104 -0
- package/src/components/atoms/text-input.tsx +31 -0
- package/src/components/commerce/merchant-card.test.tsx +261 -0
- package/src/components/commerce/merchant-card.tsx +2 -0
- package/src/components/commerce/product-card.test.tsx +364 -0
- package/src/components/commerce/product-link.test.tsx +483 -0
- package/src/components/commerce/quantity-selector.test.tsx +382 -0
- package/src/components/commerce/search.test.tsx +487 -0
- package/src/components/content/image-content-wrapper.test.tsx +92 -0
- package/src/components/index.ts +1 -0
- package/src/components/navigation/transition-link.test.tsx +155 -0
- package/src/components/ui/input.test.tsx +21 -0
- package/src/components/ui/input.tsx +10 -1
- package/src/hooks/content/useCreateImageContent.test.ts +352 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/navigation/useNavigateWithTransition.test.ts +371 -0
- package/src/hooks/navigation/useViewTransitions.test.ts +469 -0
- package/src/hooks/product/useProductSearch.test.ts +470 -0
- package/src/hooks/storage/useAsyncStorage.test.ts +225 -0
- package/src/hooks/storage/useImageUpload.test.ts +322 -0
- package/src/hooks/util/useKeyboardAvoidingView.ts +37 -0
- package/src/internal/useHandleAction.test.ts +265 -0
- package/src/internal/useShopActionsDataFetching.test.ts +465 -0
- package/src/mocks.ts +3 -1
- package/src/providers/ImagePickerProvider.test.tsx +467 -0
- package/src/stories/ProductCard.stories.tsx +2 -2
- package/src/stories/TextInput.stories.tsx +26 -0
- package/src/test-setup.ts +34 -0
- package/src/test-utils.tsx +167 -0
- 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
|
+
})
|