@shopify/shop-minis-react 0.0.34 → 0.0.36

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 (63) hide show
  1. package/dist/_virtual/index2.js +4 -4
  2. package/dist/_virtual/index3.js +4 -4
  3. package/dist/components/atoms/alert-dialog.js.map +1 -1
  4. package/dist/components/atoms/icon-button.js +12 -12
  5. package/dist/components/atoms/icon-button.js.map +1 -1
  6. package/dist/components/atoms/text-input.js +22 -0
  7. package/dist/components/atoms/text-input.js.map +1 -0
  8. package/dist/components/commerce/merchant-card.js +1 -0
  9. package/dist/components/commerce/merchant-card.js.map +1 -1
  10. package/dist/components/ui/input.js +15 -9
  11. package/dist/components/ui/input.js.map +1 -1
  12. package/dist/hooks/util/useKeyboardAvoidingView.js +23 -0
  13. package/dist/hooks/util/useKeyboardAvoidingView.js.map +1 -0
  14. package/dist/hooks/util/useShare.js +7 -6
  15. package/dist/hooks/util/useShare.js.map +1 -1
  16. package/dist/index.js +228 -222
  17. package/dist/index.js.map +1 -1
  18. package/dist/mocks.js +17 -10
  19. package/dist/mocks.js.map +1 -1
  20. package/dist/shop-minis-platform/src/types/share.js +5 -0
  21. package/dist/shop-minis-platform/src/types/share.js.map +1 -0
  22. package/dist/shop-minis-react/node_modules/.pnpm/video.js@8.23.3/node_modules/video.js/dist/video.es.js +1 -1
  23. package/dist/utils/colors.js +1 -1
  24. package/dist/utils/image.js +5 -4
  25. package/dist/utils/image.js.map +1 -1
  26. package/package.json +21 -4
  27. package/src/components/atoms/alert-dialog.test.tsx +67 -0
  28. package/src/components/atoms/alert-dialog.tsx +13 -11
  29. package/src/components/atoms/favorite-button.test.tsx +56 -0
  30. package/src/components/atoms/icon-button.tsx +1 -1
  31. package/src/components/atoms/image.test.tsx +108 -0
  32. package/src/components/atoms/product-variant-price.test.tsx +128 -0
  33. package/src/components/atoms/text-input.test.tsx +104 -0
  34. package/src/components/atoms/text-input.tsx +31 -0
  35. package/src/components/commerce/merchant-card.test.tsx +261 -0
  36. package/src/components/commerce/merchant-card.tsx +2 -0
  37. package/src/components/commerce/product-card.test.tsx +364 -0
  38. package/src/components/commerce/product-link.test.tsx +483 -0
  39. package/src/components/commerce/quantity-selector.test.tsx +382 -0
  40. package/src/components/commerce/search.test.tsx +487 -0
  41. package/src/components/content/image-content-wrapper.test.tsx +92 -0
  42. package/src/components/index.ts +1 -0
  43. package/src/components/navigation/transition-link.test.tsx +155 -0
  44. package/src/components/ui/input.test.tsx +21 -0
  45. package/src/components/ui/input.tsx +10 -1
  46. package/src/hooks/content/useCreateImageContent.test.ts +352 -0
  47. package/src/hooks/index.ts +1 -0
  48. package/src/hooks/navigation/useNavigateWithTransition.test.ts +371 -0
  49. package/src/hooks/navigation/useViewTransitions.test.ts +469 -0
  50. package/src/hooks/product/useProductSearch.test.ts +470 -0
  51. package/src/hooks/storage/useAsyncStorage.test.ts +225 -0
  52. package/src/hooks/storage/useImageUpload.test.ts +322 -0
  53. package/src/hooks/util/useKeyboardAvoidingView.ts +37 -0
  54. package/src/hooks/util/useShare.ts +13 -3
  55. package/src/internal/useHandleAction.test.ts +265 -0
  56. package/src/internal/useShopActionsDataFetching.test.ts +465 -0
  57. package/src/mocks.ts +7 -1
  58. package/src/providers/ImagePickerProvider.test.tsx +467 -0
  59. package/src/stories/ProductCard.stories.tsx +2 -2
  60. package/src/stories/TextInput.stories.tsx +26 -0
  61. package/src/test-setup.ts +34 -0
  62. package/src/test-utils.tsx +167 -0
  63. package/src/utils/image.ts +1 -0
@@ -0,0 +1,128 @@
1
+ import {describe, expect, it, vi} from 'vitest'
2
+
3
+ import {render, screen} from '../../test-utils'
4
+
5
+ import {ProductVariantPrice} from './product-variant-price'
6
+
7
+ // Mock formatMoney function
8
+ vi.mock('../../lib/formatMoney', () => ({
9
+ formatMoney: vi.fn((amount: string, currencyCode: string) => {
10
+ const numAmount = parseFloat(amount)
11
+ return currencyCode === 'USD'
12
+ ? `$${numAmount.toFixed(2)}`
13
+ : `${currencyCode} ${numAmount.toFixed(2)}`
14
+ }),
15
+ }))
16
+
17
+ describe('ProductVariantPrice', () => {
18
+ it('renders nothing when amount is missing', () => {
19
+ const {container} = render(
20
+ <ProductVariantPrice amount="" currencyCode="USD" />
21
+ )
22
+
23
+ expect(container.firstChild).toBeNull()
24
+ })
25
+
26
+ it('renders nothing when currencyCode is missing', () => {
27
+ const {container} = render(
28
+ <ProductVariantPrice amount="19.99" currencyCode="" />
29
+ )
30
+
31
+ expect(container.firstChild).toBeNull()
32
+ })
33
+
34
+ it('renders single price when no compare at price', () => {
35
+ render(<ProductVariantPrice amount="19.99" currencyCode="USD" />)
36
+
37
+ const price = screen.getByText('$19.99')
38
+ expect(price).not.toHaveClass('line-through')
39
+ })
40
+
41
+ it('renders both prices when compare at price is provided and different', () => {
42
+ render(
43
+ <ProductVariantPrice
44
+ amount="19.99"
45
+ currencyCode="USD"
46
+ compareAtPriceAmount="29.99"
47
+ />
48
+ )
49
+
50
+ expect(screen.getByText('$19.99')).toBeInTheDocument()
51
+ expect(screen.getByText('$29.99')).toBeInTheDocument()
52
+
53
+ const compareAtPrice = screen.getByText('$29.99')
54
+ expect(compareAtPrice).toHaveClass('line-through')
55
+ })
56
+
57
+ it('renders single price when compare at price equals current price', () => {
58
+ render(
59
+ <ProductVariantPrice
60
+ amount="19.99"
61
+ currencyCode="USD"
62
+ compareAtPriceAmount="19.99"
63
+ />
64
+ )
65
+
66
+ const prices = screen.getAllByText('$19.99')
67
+ expect(prices).toHaveLength(1)
68
+ expect(prices[0]).not.toHaveClass('line-through')
69
+ })
70
+
71
+ it('handles different currency codes', () => {
72
+ render(
73
+ <ProductVariantPrice
74
+ amount="19.99"
75
+ currencyCode="EUR"
76
+ compareAtPriceAmount="29.99"
77
+ compareAtPriceCurrencyCode="GBP"
78
+ />
79
+ )
80
+
81
+ expect(screen.getByText('EUR 19.99')).toBeInTheDocument()
82
+ expect(screen.getByText('GBP 29.99')).toBeInTheDocument()
83
+ })
84
+
85
+ it('applies custom className', () => {
86
+ render(
87
+ <ProductVariantPrice
88
+ amount="19.99"
89
+ currencyCode="USD"
90
+ className="custom-class"
91
+ />
92
+ )
93
+
94
+ const container = screen.getByText('$19.99').parentElement
95
+ expect(container).toHaveClass('custom-class')
96
+ })
97
+
98
+ it('applies custom price classNames', () => {
99
+ render(
100
+ <ProductVariantPrice
101
+ amount="19.99"
102
+ currencyCode="USD"
103
+ compareAtPriceAmount="29.99"
104
+ currentPriceClassName="current-custom"
105
+ originalPriceClassName="original-custom"
106
+ />
107
+ )
108
+
109
+ const currentPrice = screen.getByText('$19.99')
110
+ expect(currentPrice).toHaveClass('current-custom')
111
+
112
+ const originalPrice = screen.getByText('$29.99')
113
+ expect(originalPrice).toHaveClass('original-custom')
114
+ })
115
+
116
+ it('handles string and number amounts', () => {
117
+ render(
118
+ <ProductVariantPrice
119
+ amount={19.99}
120
+ currencyCode="USD"
121
+ compareAtPriceAmount={29.99}
122
+ />
123
+ )
124
+
125
+ expect(screen.getByText('$19.99')).toBeInTheDocument()
126
+ expect(screen.getByText('$29.99')).toBeInTheDocument()
127
+ })
128
+ })
@@ -0,0 +1,104 @@
1
+ import {describe, expect, it, vi, beforeEach} from 'vitest'
2
+
3
+ import {useShopActions} from '../../internal/useShopActions'
4
+ import {render, screen, userEvent} from '../../test-utils'
5
+
6
+ import {TextInput} from './text-input'
7
+
8
+ vi.mock('../../internal/useShopActions', () => ({
9
+ useShopActions: vi.fn(() => ({
10
+ translateContentUp: vi.fn(),
11
+ translateContentDown: vi.fn(),
12
+ })),
13
+ }))
14
+
15
+ describe('TextInput', () => {
16
+ let mockTranslateContentUp: ReturnType<typeof vi.fn>
17
+ let mockTranslateContentDown: ReturnType<typeof vi.fn>
18
+
19
+ beforeEach(() => {
20
+ vi.clearAllMocks()
21
+
22
+ mockTranslateContentUp = vi.fn()
23
+ mockTranslateContentDown = vi.fn()
24
+ ;(useShopActions as ReturnType<typeof vi.fn>).mockReturnValue({
25
+ translateContentUp: mockTranslateContentUp,
26
+ translateContentDown: mockTranslateContentDown,
27
+ })
28
+ })
29
+
30
+ it('forwards all input props correctly', () => {
31
+ render(
32
+ <TextInput
33
+ placeholder="Test input"
34
+ value="test value"
35
+ disabled
36
+ type="email"
37
+ className="custom-class"
38
+ id="test-input"
39
+ />
40
+ )
41
+
42
+ const input = screen.getByPlaceholderText('Test input')
43
+ expect(input).toHaveValue('test value')
44
+ expect(input).toBeDisabled()
45
+ expect(input).toHaveAttribute('type', 'email')
46
+ expect(input).toHaveClass('custom-class')
47
+ expect(input).toHaveAttribute('id', 'test-input')
48
+
49
+ expect(input).toBeInstanceOf(HTMLInputElement)
50
+ expect(input.tagName).toBe('INPUT')
51
+ })
52
+
53
+ it('calls translateContentUp on focus', async () => {
54
+ const user = userEvent.setup()
55
+
56
+ render(<TextInput placeholder="Test input" />)
57
+
58
+ const input = screen.getByPlaceholderText('Test input')
59
+
60
+ await user.click(input)
61
+
62
+ expect(mockTranslateContentUp).toHaveBeenCalledTimes(1)
63
+ })
64
+
65
+ it('calls translateContentDown on blur', async () => {
66
+ const user = userEvent.setup()
67
+
68
+ render(<TextInput placeholder="Test input" />)
69
+
70
+ const input = screen.getByPlaceholderText('Test input')
71
+
72
+ await user.click(input)
73
+ await user.tab()
74
+
75
+ expect(mockTranslateContentDown).toHaveBeenCalledTimes(1)
76
+ })
77
+
78
+ it('calls custom onFocus handler when provided', async () => {
79
+ const user = userEvent.setup()
80
+ const customOnFocus = vi.fn()
81
+
82
+ render(<TextInput placeholder="Test input" onFocus={customOnFocus} />)
83
+
84
+ const input = screen.getByPlaceholderText('Test input')
85
+ await user.click(input)
86
+
87
+ expect(customOnFocus).toHaveBeenCalledTimes(1)
88
+ })
89
+
90
+ it('calls custom onBlur handler when provided', async () => {
91
+ const user = userEvent.setup()
92
+ const customOnBlur = vi.fn()
93
+
94
+ render(<TextInput placeholder="Test input" onBlur={customOnBlur} />)
95
+
96
+ const input = screen.getByPlaceholderText('Test input')
97
+
98
+ // Focus first, then blur
99
+ await user.click(input)
100
+ await user.tab()
101
+
102
+ expect(customOnBlur).toHaveBeenCalledTimes(1)
103
+ })
104
+ })
@@ -0,0 +1,31 @@
1
+ import * as React from 'react'
2
+
3
+ import {useKeyboardAvoidingView} from '../../hooks'
4
+ import {Input} from '../ui/input'
5
+
6
+ function TextInput({...props}: React.ComponentProps<'input'>) {
7
+ const inputRef = React.useRef<HTMLInputElement>(null)
8
+ const {onBlur, onFocus} = useKeyboardAvoidingView()
9
+
10
+ const _onFocus = React.useCallback(
11
+ (event: React.FocusEvent<HTMLInputElement>) => {
12
+ onFocus(inputRef)
13
+ props.onFocus?.(event)
14
+ },
15
+ [props, onFocus, inputRef]
16
+ )
17
+
18
+ const _onBlur = React.useCallback(
19
+ (event: React.FocusEvent<HTMLInputElement>) => {
20
+ onBlur()
21
+ props.onBlur?.(event)
22
+ },
23
+ [props, onBlur]
24
+ )
25
+
26
+ return (
27
+ <Input innerRef={inputRef} onFocus={_onFocus} onBlur={_onBlur} {...props} />
28
+ )
29
+ }
30
+
31
+ export {TextInput}
@@ -0,0 +1,261 @@
1
+ import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest'
2
+
3
+ import {
4
+ render,
5
+ screen,
6
+ userEvent,
7
+ mockShop,
8
+ mockMinisSDK,
9
+ resetAllMocks,
10
+ } from '../../test-utils'
11
+
12
+ import {
13
+ MerchantCard,
14
+ MerchantCardContainer,
15
+ MerchantCardHeader,
16
+ MerchantCardInfo,
17
+ MerchantCardName,
18
+ MerchantCardRating,
19
+ } from './merchant-card'
20
+
21
+ // Mock hooks
22
+ vi.mock('../../hooks/navigation/useShopNavigation', () => ({
23
+ useShopNavigation: () => ({
24
+ navigateToShop: mockMinisSDK.navigateToShop,
25
+ navigateToProduct: mockMinisSDK.navigateToProduct,
26
+ }),
27
+ }))
28
+
29
+ // Mock utils with simple implementations
30
+ vi.mock('../../utils', () => ({
31
+ extractBrandTheme: vi.fn(() => ({
32
+ type: 'default',
33
+ backgroundColor: 'white',
34
+ })),
35
+ getFeaturedImages: vi.fn(() => []),
36
+ formatReviewCount: vi.fn((count: number) => {
37
+ if (count >= 1000) return `${(count / 1000).toFixed(1)}k`
38
+ return count.toString()
39
+ }),
40
+ normalizeRating: vi.fn((rating: number) => rating.toFixed(1)),
41
+ }))
42
+
43
+ vi.mock('../../utils/colors', () => ({
44
+ isDarkColor: vi.fn(() => false),
45
+ }))
46
+
47
+ describe('MerchantCard', () => {
48
+ beforeEach(() => {
49
+ resetAllMocks()
50
+ })
51
+
52
+ afterEach(() => {
53
+ vi.clearAllMocks()
54
+ })
55
+
56
+ describe('Rendering', () => {
57
+ it('renders with basic shop information', () => {
58
+ const shop = mockShop()
59
+ render(<MerchantCard shop={shop} />)
60
+
61
+ expect(screen.getByText(shop.name)).toBeInTheDocument()
62
+ // createShop returns 4.3 rating with 50 reviews
63
+ expect(screen.getByText('4.3 (50)')).toBeInTheDocument() // Rating and review count
64
+ })
65
+
66
+ it('renders shop logo when available', () => {
67
+ const shop = mockShop({
68
+ visualTheme: {
69
+ logoImage: {
70
+ url: 'https://example.com/logo.png',
71
+ altText: 'Shop Logo',
72
+ sensitive: false,
73
+ },
74
+ featuredImages: [],
75
+ id: '123',
76
+ },
77
+ })
78
+
79
+ render(<MerchantCard shop={shop} />)
80
+
81
+ const logo = screen.getByAltText(`${shop.name} logo`)
82
+ expect(logo).toBeInTheDocument()
83
+ expect(logo).toHaveAttribute('src', 'https://example.com/logo.png')
84
+ })
85
+
86
+ it('renders shop initial when logo is not available', () => {
87
+ const shop = mockShop({
88
+ name: 'Test Shop',
89
+ visualTheme: null,
90
+ })
91
+
92
+ render(<MerchantCard shop={shop} />)
93
+
94
+ expect(screen.getByText('T')).toBeInTheDocument()
95
+ })
96
+
97
+ it('renders without reviews when not available', () => {
98
+ const shop = mockShop({
99
+ reviewAnalytics: {
100
+ averageRating: null,
101
+ reviewCount: 0,
102
+ },
103
+ })
104
+
105
+ render(<MerchantCard shop={shop} />)
106
+
107
+ expect(
108
+ screen.queryByTestId('merchant-card-rating')
109
+ ).not.toBeInTheDocument()
110
+ })
111
+ })
112
+
113
+ describe('Interactions', () => {
114
+ it('handles shop click navigation', async () => {
115
+ const user = userEvent.setup()
116
+ const shop = mockShop()
117
+
118
+ render(<MerchantCard shop={shop} />)
119
+
120
+ const card = screen.getByText(shop.name).closest('div')
121
+ ?.parentElement?.parentElement
122
+ await user.click(card!)
123
+
124
+ expect(mockMinisSDK.navigateToShop).toHaveBeenCalledWith({
125
+ shopId: shop.id,
126
+ })
127
+ })
128
+
129
+ it('does not navigate when touchable is false', async () => {
130
+ const user = userEvent.setup()
131
+ const shop = mockShop()
132
+
133
+ render(<MerchantCard shop={shop} touchable={false} />)
134
+
135
+ const card = screen.getByText(shop.name).closest('div')
136
+ ?.parentElement?.parentElement
137
+ await user.click(card!)
138
+
139
+ expect(mockMinisSDK.navigateToShop).not.toHaveBeenCalled()
140
+ })
141
+ })
142
+
143
+ describe('Custom Composition', () => {
144
+ it('renders with custom children layout', () => {
145
+ const shop = mockShop()
146
+
147
+ render(
148
+ <MerchantCard shop={shop}>
149
+ <MerchantCardContainer>
150
+ <div data-testid="custom-layout">
151
+ <MerchantCardName>Custom Name</MerchantCardName>
152
+ <MerchantCardRating />
153
+ </div>
154
+ </MerchantCardContainer>
155
+ </MerchantCard>
156
+ )
157
+
158
+ expect(screen.getByTestId('custom-layout')).toBeInTheDocument()
159
+ expect(screen.getByText('Custom Name')).toBeInTheDocument()
160
+ // createShop returns 4.3 rating with 50 reviews
161
+ expect(screen.getByText('4.3 (50)')).toBeInTheDocument()
162
+ })
163
+
164
+ it('allows individual component usage', () => {
165
+ const shop = mockShop()
166
+
167
+ render(
168
+ <MerchantCard shop={shop}>
169
+ <MerchantCardContainer>
170
+ <MerchantCardHeader withLogo />
171
+ <MerchantCardInfo>
172
+ <MerchantCardName />
173
+ <MerchantCardRating />
174
+ </MerchantCardInfo>
175
+ </MerchantCardContainer>
176
+ </MerchantCard>
177
+ )
178
+
179
+ expect(screen.getByText(shop.name)).toBeInTheDocument()
180
+ // createShop returns 4.3 rating with 50 reviews
181
+ expect(screen.getByText('4.3 (50)')).toBeInTheDocument()
182
+ })
183
+ })
184
+
185
+ describe('Edge Cases', () => {
186
+ it('handles shop without visual theme', () => {
187
+ const shop = mockShop({
188
+ visualTheme: null,
189
+ })
190
+
191
+ render(<MerchantCard shop={shop} />)
192
+
193
+ expect(screen.getByText(shop.name)).toBeInTheDocument()
194
+ })
195
+
196
+ it('handles shop with empty name', () => {
197
+ const shop = mockShop({
198
+ name: '',
199
+ })
200
+
201
+ render(<MerchantCard shop={shop} />)
202
+
203
+ // Should still render with empty name
204
+ const heading = screen.getByRole('heading', {level: 3})
205
+ expect(heading).toBeInTheDocument()
206
+ })
207
+
208
+ it('handles very long shop names', () => {
209
+ const shop = mockShop({
210
+ name: 'This is a very long shop name that should be truncated in the UI',
211
+ })
212
+
213
+ render(<MerchantCard shop={shop} />)
214
+
215
+ const nameElement = screen.getByText(shop.name)
216
+ expect(nameElement).toHaveClass('truncate')
217
+ })
218
+
219
+ it('handles high review counts', () => {
220
+ const shop = mockShop({
221
+ reviewAnalytics: {
222
+ averageRating: 4.8,
223
+ reviewCount: 12500,
224
+ },
225
+ })
226
+
227
+ render(<MerchantCard shop={shop} />)
228
+
229
+ expect(screen.getByText('4.8 (12.5k)')).toBeInTheDocument()
230
+ })
231
+ })
232
+
233
+ describe('Accessibility', () => {
234
+ it('maintains proper heading hierarchy', () => {
235
+ const shop = mockShop()
236
+ render(<MerchantCard shop={shop} />)
237
+
238
+ const heading = screen.getByRole('heading', {level: 3})
239
+ expect(heading).toHaveTextContent(shop.name)
240
+ })
241
+
242
+ it('provides fallback alt text for logo', () => {
243
+ const shop = mockShop({
244
+ visualTheme: {
245
+ logoImage: {
246
+ url: 'https://example.com/logo.png',
247
+ altText: null,
248
+ sensitive: false,
249
+ },
250
+ featuredImages: [],
251
+ id: '123',
252
+ },
253
+ })
254
+
255
+ render(<MerchantCard shop={shop} />)
256
+
257
+ const logo = screen.getByAltText(`${shop.name} logo`)
258
+ expect(logo).toBeInTheDocument()
259
+ })
260
+ })
261
+ })
@@ -274,6 +274,8 @@ function MerchantCardDefaultHeader({withLogo = false}: {withLogo?: boolean}) {
274
274
  />
275
275
  )
276
276
  }
277
+
278
+ return null
277
279
  }
278
280
 
279
281
  return (