@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,465 @@
1
+ import {renderHook, act, waitFor} from '@testing-library/react'
2
+ import {describe, expect, it, vi, beforeEach} from 'vitest'
3
+
4
+ import {useShopActionsDataFetching} from './useShopActionsDataFetching'
5
+
6
+ // Mock the error formatter
7
+ vi.mock('../utils/errors', () => ({
8
+ formatError: vi.fn((_, error) => {
9
+ if (error instanceof Error) return error
10
+ return new Error(String(error))
11
+ }),
12
+ MiniError: class MiniError extends Error {
13
+ constructor({message}: {message: string; hook?: string}) {
14
+ super(message)
15
+ this.name = 'MiniError'
16
+ }
17
+ },
18
+ }))
19
+
20
+ describe('useShopActionsDataFetching', () => {
21
+ let mockAction: ReturnType<typeof vi.fn>
22
+
23
+ beforeEach(() => {
24
+ vi.clearAllMocks()
25
+ mockAction = vi.fn()
26
+ })
27
+
28
+ describe('Initial Fetch', () => {
29
+ it('fetches data on mount', async () => {
30
+ const mockData = {data: {id: '1', name: 'Test'}}
31
+ mockAction.mockResolvedValue({
32
+ ok: true,
33
+ data: mockData,
34
+ })
35
+
36
+ const {result} = renderHook(() =>
37
+ useShopActionsDataFetching(mockAction, {}, {})
38
+ )
39
+
40
+ // Initially loading
41
+ expect(result.current.loading).toBe(true)
42
+ expect(result.current.data).toBeNull()
43
+ expect(result.current.error).toBeNull()
44
+
45
+ // Wait for fetch to complete
46
+ await waitFor(() => {
47
+ expect(result.current.loading).toBe(false)
48
+ })
49
+
50
+ expect(result.current.data).toEqual({id: '1', name: 'Test'})
51
+ expect(result.current.error).toBeNull()
52
+ expect(mockAction).toHaveBeenCalledWith({})
53
+ })
54
+
55
+ it('handles initial fetch error', async () => {
56
+ const errorMessage = 'Network error'
57
+ mockAction.mockResolvedValue({
58
+ ok: false,
59
+ error: new Error(errorMessage),
60
+ })
61
+
62
+ const {result} = renderHook(() =>
63
+ useShopActionsDataFetching(mockAction, {}, {})
64
+ )
65
+
66
+ await waitFor(() => {
67
+ expect(result.current.loading).toBe(false)
68
+ })
69
+
70
+ expect(result.current.data).toBeNull()
71
+ expect(result.current.error).toBeInstanceOf(Error)
72
+ expect(result.current.error?.message).toBe(errorMessage)
73
+ })
74
+
75
+ it('skips initial fetch when skip is true', async () => {
76
+ mockAction.mockResolvedValue({
77
+ ok: true,
78
+ data: {data: 'test'},
79
+ })
80
+
81
+ const {result} = renderHook(() =>
82
+ useShopActionsDataFetching(mockAction, {}, {skip: true})
83
+ )
84
+
85
+ // Should not be loading when skipped
86
+ expect(result.current.loading).toBe(true) // Initially true regardless of skip
87
+ expect(result.current.data).toBeNull()
88
+
89
+ // Wait a bit to ensure no fetch happens
90
+ await new Promise(resolve => setTimeout(resolve, 50))
91
+
92
+ expect(mockAction).not.toHaveBeenCalled()
93
+ expect(result.current.data).toBeNull()
94
+ })
95
+ })
96
+
97
+ describe('Params Changes', () => {
98
+ it('refetches when params change', async () => {
99
+ const mockData1 = {data: {id: '1', value: 'first'}}
100
+ const mockData2 = {data: {id: '2', value: 'second'}}
101
+
102
+ mockAction
103
+ .mockResolvedValueOnce({ok: true, data: mockData1})
104
+ .mockResolvedValueOnce({ok: true, data: mockData2})
105
+
106
+ const {result, rerender} = renderHook(
107
+ ({params}) => useShopActionsDataFetching(mockAction, params, {}),
108
+ {initialProps: {params: {id: '1'}}}
109
+ )
110
+
111
+ await waitFor(() => {
112
+ expect(result.current.loading).toBe(false)
113
+ })
114
+
115
+ expect(result.current.data).toEqual({id: '1', value: 'first'})
116
+ expect(mockAction).toHaveBeenCalledWith({id: '1'})
117
+
118
+ // Change params
119
+ rerender({params: {id: '2'}})
120
+
121
+ await waitFor(() => {
122
+ expect(result.current.data).toEqual({id: '2', value: 'second'})
123
+ })
124
+
125
+ expect(mockAction).toHaveBeenCalledWith({id: '2'})
126
+ expect(mockAction).toHaveBeenCalledTimes(2)
127
+ })
128
+
129
+ it('does not refetch when params are structurally equal', async () => {
130
+ const mockData = {data: {id: '1', name: 'Test'}}
131
+ mockAction.mockResolvedValue({ok: true, data: mockData})
132
+
133
+ const {result, rerender} = renderHook(
134
+ ({params}) => useShopActionsDataFetching(mockAction, params, {}),
135
+ {initialProps: {params: {id: '1', nested: {value: 'test'}}}}
136
+ )
137
+
138
+ await waitFor(() => {
139
+ expect(result.current.loading).toBe(false)
140
+ })
141
+
142
+ expect(mockAction).toHaveBeenCalledTimes(1)
143
+
144
+ // Rerender with structurally equal params (new object reference)
145
+ rerender({params: {id: '1', nested: {value: 'test'}}})
146
+
147
+ // Wait a bit to ensure no additional fetch
148
+ await new Promise(resolve => setTimeout(resolve, 50))
149
+
150
+ expect(mockAction).toHaveBeenCalledTimes(1)
151
+ })
152
+ })
153
+
154
+ describe('Refetch', () => {
155
+ it('refetches data without setting loading', async () => {
156
+ const mockData1 = {data: {id: '1', value: 'initial'}}
157
+ const mockData2 = {data: {id: '1', value: 'refetched'}}
158
+
159
+ mockAction
160
+ .mockResolvedValueOnce({ok: true, data: mockData1})
161
+ .mockResolvedValueOnce({ok: true, data: mockData2})
162
+
163
+ const {result} = renderHook(() =>
164
+ useShopActionsDataFetching(mockAction, {}, {})
165
+ )
166
+
167
+ await waitFor(() => {
168
+ expect(result.current.loading).toBe(false)
169
+ })
170
+
171
+ expect(result.current.data).toEqual({id: '1', value: 'initial'})
172
+
173
+ // Track loading state during refetch
174
+ let loadingDuringRefetch = false
175
+
176
+ await act(async () => {
177
+ const refetchPromise = result.current.refetch()
178
+
179
+ // Check that loading is not set
180
+ loadingDuringRefetch = result.current.loading
181
+
182
+ await refetchPromise
183
+ })
184
+
185
+ expect(loadingDuringRefetch).toBe(false)
186
+ expect(result.current.data).toEqual({id: '1', value: 'refetched'})
187
+ expect(mockAction).toHaveBeenCalledTimes(2)
188
+ expect(mockAction).toHaveBeenLastCalledWith({fetchPolicy: 'network-only'})
189
+ })
190
+
191
+ it('throws error on refetch failure', async () => {
192
+ const mockData = {data: {id: '1', value: 'initial'}}
193
+ const refetchError = new Error('Refetch failed')
194
+
195
+ mockAction
196
+ .mockResolvedValueOnce({ok: true, data: mockData})
197
+ .mockResolvedValueOnce({ok: false, error: refetchError})
198
+
199
+ const {result} = renderHook(() =>
200
+ useShopActionsDataFetching(mockAction, {}, {})
201
+ )
202
+
203
+ await waitFor(() => {
204
+ expect(result.current.loading).toBe(false)
205
+ })
206
+
207
+ expect(result.current.data).toEqual({id: '1', value: 'initial'})
208
+
209
+ await act(async () => {
210
+ await expect(result.current.refetch()).rejects.toThrow('Refetch failed')
211
+ })
212
+
213
+ // Data should remain unchanged on refetch error
214
+ expect(result.current.data).toEqual({id: '1', value: 'initial'})
215
+ expect(result.current.error).toBeInstanceOf(Error)
216
+ })
217
+
218
+ it('maintains data on refetch error', async () => {
219
+ const initialData = {data: {id: '1', value: 'initial'}}
220
+
221
+ mockAction
222
+ .mockResolvedValueOnce({ok: true, data: initialData})
223
+ .mockResolvedValueOnce({ok: false, error: new Error('Refetch error')})
224
+
225
+ const {result} = renderHook(() =>
226
+ useShopActionsDataFetching(mockAction, {}, {})
227
+ )
228
+
229
+ await waitFor(() => {
230
+ expect(result.current.loading).toBe(false)
231
+ })
232
+
233
+ const originalData = result.current.data
234
+
235
+ await act(async () => {
236
+ try {
237
+ await result.current.refetch()
238
+ } catch {
239
+ // Expected error
240
+ }
241
+ })
242
+
243
+ // Data should not be reset on refetch error
244
+ expect(result.current.data).toBe(originalData)
245
+ })
246
+ })
247
+
248
+ describe('Validation', () => {
249
+ it('validates data and sets error on validation failure', async () => {
250
+ const mockData = {data: {id: '1', value: 'test'}}
251
+ mockAction.mockResolvedValue({ok: true, data: mockData})
252
+
253
+ const validator = vi.fn(data => {
254
+ if (data.value === 'test') {
255
+ throw new Error('Invalid value')
256
+ }
257
+ })
258
+
259
+ const {result} = renderHook(() =>
260
+ useShopActionsDataFetching(
261
+ mockAction,
262
+ {},
263
+ {validator, hook: 'testHook'}
264
+ )
265
+ )
266
+
267
+ await waitFor(() => {
268
+ expect(result.current.loading).toBe(false)
269
+ })
270
+
271
+ // When validation fails and validation error is set, data is not returned
272
+ expect(result.current.data).toBeNull()
273
+ expect(result.current.error).toBeInstanceOf(Error)
274
+ expect(result.current.error?.message).toBe('Invalid value')
275
+ expect(validator).toHaveBeenCalledWith({id: '1', value: 'test'})
276
+ })
277
+
278
+ it('passes validation when no error is thrown', async () => {
279
+ const mockData = {data: {id: '1', value: 'valid'}}
280
+ mockAction.mockResolvedValue({ok: true, data: mockData})
281
+
282
+ const validator = vi.fn(data => {
283
+ // No error thrown - validation passes
284
+ expect(data).toBeDefined()
285
+ })
286
+
287
+ const {result} = renderHook(() =>
288
+ useShopActionsDataFetching(mockAction, {}, {validator})
289
+ )
290
+
291
+ await waitFor(() => {
292
+ expect(result.current.loading).toBe(false)
293
+ })
294
+
295
+ expect(result.current.data).toEqual({id: '1', value: 'valid'})
296
+ expect(result.current.error).toBeNull()
297
+ expect(validator).toHaveBeenCalledWith({id: '1', value: 'valid'})
298
+ })
299
+
300
+ it('handles non-Error validation failures', async () => {
301
+ const mockData = {data: {id: '1', value: 'test'}}
302
+ mockAction.mockResolvedValue({ok: true, data: mockData})
303
+
304
+ const validator = vi.fn(() => {
305
+ // eslint-disable-next-line no-throw-literal
306
+ throw 'String error' // Non-Error thrown
307
+ })
308
+
309
+ const {result} = renderHook(() =>
310
+ useShopActionsDataFetching(
311
+ mockAction,
312
+ {},
313
+ {validator, hook: 'testHook'}
314
+ )
315
+ )
316
+
317
+ await waitFor(() => {
318
+ expect(result.current.loading).toBe(false)
319
+ })
320
+
321
+ expect(result.current.error).toBeInstanceOf(Error)
322
+ expect(result.current.error?.message).toBe('Validation failed')
323
+ })
324
+ })
325
+
326
+ describe('Error Handling', () => {
327
+ it('formats errors properly', async () => {
328
+ const originalError = {
329
+ code: 'TEST_ERROR',
330
+ message: 'Test error message',
331
+ }
332
+
333
+ mockAction.mockResolvedValue({
334
+ ok: false,
335
+ error: originalError,
336
+ })
337
+
338
+ const {result} = renderHook(() =>
339
+ useShopActionsDataFetching(mockAction, {}, {hook: 'testHook'})
340
+ )
341
+
342
+ await waitFor(() => {
343
+ expect(result.current.loading).toBe(false)
344
+ })
345
+
346
+ expect(result.current.error).toBeDefined()
347
+ expect(result.current.data).toBeNull()
348
+ })
349
+
350
+ it('resets error on successful fetch after error', async () => {
351
+ const errorResponse = {ok: false as const, error: new Error('Error')}
352
+ const successResponse = {ok: true as const, data: {data: 'success'}}
353
+
354
+ mockAction
355
+ .mockResolvedValueOnce(errorResponse)
356
+ .mockResolvedValueOnce(successResponse)
357
+
358
+ const {result, rerender} = renderHook(
359
+ ({params}) => useShopActionsDataFetching(mockAction, params, {}),
360
+ {initialProps: {params: {id: '1'}}}
361
+ )
362
+
363
+ await waitFor(() => {
364
+ expect(result.current.error).toBeDefined()
365
+ })
366
+
367
+ // Change params to trigger new fetch
368
+ rerender({params: {id: '2'}})
369
+
370
+ await waitFor(() => {
371
+ expect(result.current.error).toBeNull()
372
+ })
373
+
374
+ expect(result.current.data).toBe('success')
375
+ })
376
+ })
377
+
378
+ describe('Fetch Policy', () => {
379
+ it('includes fetchPolicy in action params', async () => {
380
+ mockAction.mockResolvedValue({
381
+ ok: true,
382
+ data: {data: 'test'},
383
+ })
384
+
385
+ renderHook(() =>
386
+ useShopActionsDataFetching(mockAction, {fetchPolicy: 'cache-first'}, {})
387
+ )
388
+
389
+ await waitFor(() => {
390
+ expect(mockAction).toHaveBeenCalledWith({fetchPolicy: 'cache-first'})
391
+ })
392
+ })
393
+
394
+ it('overrides fetchPolicy in refetch', async () => {
395
+ mockAction.mockResolvedValue({
396
+ ok: true,
397
+ data: {data: 'test'},
398
+ })
399
+
400
+ const {result} = renderHook(() =>
401
+ useShopActionsDataFetching(mockAction, {fetchPolicy: 'cache-first'}, {})
402
+ )
403
+
404
+ await waitFor(() => {
405
+ expect(result.current.loading).toBe(false)
406
+ })
407
+
408
+ await act(async () => {
409
+ await result.current.refetch()
410
+ })
411
+
412
+ expect(mockAction).toHaveBeenLastCalledWith({
413
+ fetchPolicy: 'network-only',
414
+ })
415
+ })
416
+ })
417
+
418
+ describe('Hook Lifecycle', () => {
419
+ it('cleans up properly on unmount', async () => {
420
+ mockAction.mockResolvedValue({
421
+ ok: true,
422
+ data: {data: 'test'},
423
+ })
424
+
425
+ const {unmount} = renderHook(() =>
426
+ useShopActionsDataFetching(mockAction, {}, {})
427
+ )
428
+
429
+ await waitFor(() => {
430
+ expect(mockAction).toHaveBeenCalled()
431
+ })
432
+
433
+ unmount()
434
+
435
+ // Ensure no additional calls after unmount
436
+ expect(mockAction).toHaveBeenCalledTimes(1)
437
+ })
438
+
439
+ it('handles rapid mount/unmount cycles', async () => {
440
+ let resolveFetch: ((value: any) => void) | undefined
441
+
442
+ mockAction.mockImplementation(
443
+ () =>
444
+ new Promise(resolve => {
445
+ resolveFetch = resolve
446
+ })
447
+ )
448
+
449
+ const {unmount} = renderHook(() =>
450
+ useShopActionsDataFetching(mockAction, {}, {})
451
+ )
452
+
453
+ // Unmount before fetch completes
454
+ unmount()
455
+
456
+ // Complete the fetch after unmount
457
+ if (resolveFetch) {
458
+ resolveFetch({ok: true, data: {data: 'test'}})
459
+ }
460
+
461
+ // No errors should occur
462
+ expect(mockAction).toHaveBeenCalledTimes(1)
463
+ })
464
+ })
465
+ })
package/src/mocks.ts CHANGED
@@ -183,10 +183,12 @@ function makeMockMethod<K extends keyof ShopActions>(
183
183
  }) as ShopActions[K]
184
184
  }
185
185
 
186
- function makeMockActions(): ShopActions {
186
+ export function makeMockActions(): ShopActions {
187
187
  const results: {
188
188
  [K in keyof ShopActions]: ShopActionDataType<ShopActions[K]>
189
189
  } = {
190
+ translateContentUp: undefined,
191
+ translateContentDown: undefined,
190
192
  followShop: true,
191
193
  unfollowShop: false,
192
194
  favorite: undefined,