@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,469 @@
1
+ import {renderHook, act} from '@testing-library/react'
2
+ import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest'
3
+
4
+ import {useViewTransitions} from './useViewTransitions'
5
+
6
+ // Mock react-router
7
+ const mockNavigationType = vi.fn()
8
+ const mockLocation = {pathname: '/test'}
9
+
10
+ vi.mock('react-router', () => ({
11
+ useNavigationType: () => mockNavigationType(),
12
+ useLocation: () => mockLocation,
13
+ }))
14
+
15
+ describe('useViewTransitions', () => {
16
+ let originalStartViewTransition: any
17
+ let addEventListenerSpy: ReturnType<typeof vi.spyOn>
18
+ let removeEventListenerSpy: ReturnType<typeof vi.spyOn>
19
+ let setAttributeSpy: ReturnType<typeof vi.spyOn>
20
+ let removeAttributeSpy: ReturnType<typeof vi.spyOn>
21
+
22
+ beforeEach(() => {
23
+ vi.clearAllMocks()
24
+ originalStartViewTransition = document.startViewTransition
25
+
26
+ // Reset mock navigation type
27
+ mockNavigationType.mockReturnValue('PUSH')
28
+
29
+ // Setup spies
30
+ addEventListenerSpy = vi.spyOn(window, 'addEventListener')
31
+ removeEventListenerSpy = vi.spyOn(window, 'removeEventListener')
32
+ setAttributeSpy = vi.spyOn(document.documentElement, 'setAttribute')
33
+ removeAttributeSpy = vi.spyOn(document.documentElement, 'removeAttribute')
34
+ })
35
+
36
+ afterEach(() => {
37
+ document.startViewTransition = originalStartViewTransition
38
+ document.documentElement.removeAttribute('data-navigation-type')
39
+ addEventListenerSpy.mockRestore()
40
+ removeEventListenerSpy.mockRestore()
41
+ setAttributeSpy.mockRestore()
42
+ removeAttributeSpy.mockRestore()
43
+ })
44
+
45
+ describe('Event Listeners', () => {
46
+ it('registers event listeners on mount', () => {
47
+ renderHook(() => useViewTransitions())
48
+
49
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
50
+ 'androidbackpressed',
51
+ expect.any(Function)
52
+ )
53
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
54
+ 'popstate',
55
+ expect.any(Function)
56
+ )
57
+ })
58
+
59
+ it('removes event listeners on unmount', () => {
60
+ const {unmount} = renderHook(() => useViewTransitions())
61
+
62
+ unmount()
63
+
64
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
65
+ 'popstate',
66
+ expect.any(Function)
67
+ )
68
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
69
+ 'androidbackpressed',
70
+ expect.any(Function)
71
+ )
72
+ })
73
+ })
74
+
75
+ describe('Navigation Type Attribute', () => {
76
+ it('sets forward navigation type for PUSH', () => {
77
+ mockNavigationType.mockReturnValue('PUSH')
78
+
79
+ renderHook(() => useViewTransitions())
80
+
81
+ expect(setAttributeSpy).toHaveBeenCalledWith(
82
+ 'data-navigation-type',
83
+ 'forward'
84
+ )
85
+ })
86
+
87
+ it('sets backward navigation type for POP', () => {
88
+ mockNavigationType.mockReturnValue('POP')
89
+
90
+ renderHook(() => useViewTransitions())
91
+
92
+ expect(setAttributeSpy).toHaveBeenCalledWith(
93
+ 'data-navigation-type',
94
+ 'backward'
95
+ )
96
+ })
97
+
98
+ it('does not set attribute for REPLACE navigation', () => {
99
+ mockNavigationType.mockReturnValue('REPLACE')
100
+
101
+ renderHook(() => useViewTransitions())
102
+
103
+ expect(setAttributeSpy).not.toHaveBeenCalledWith(
104
+ 'data-navigation-type',
105
+ expect.any(String)
106
+ )
107
+ })
108
+
109
+ it('does not override existing navigation type attribute', () => {
110
+ // Set existing attribute
111
+ document.documentElement.setAttribute('data-navigation-type', 'existing')
112
+ setAttributeSpy.mockClear()
113
+
114
+ mockNavigationType.mockReturnValue('PUSH')
115
+
116
+ renderHook(() => useViewTransitions())
117
+
118
+ // Should not set attribute again
119
+ expect(setAttributeSpy).not.toHaveBeenCalledWith(
120
+ 'data-navigation-type',
121
+ 'forward'
122
+ )
123
+ })
124
+
125
+ it('removes navigation type attribute on unmount', () => {
126
+ const {unmount} = renderHook(() => useViewTransitions())
127
+
128
+ unmount()
129
+
130
+ expect(removeAttributeSpy).toHaveBeenCalledWith('data-navigation-type')
131
+ })
132
+ })
133
+
134
+ describe('Android Back Press', () => {
135
+ beforeEach(() => {
136
+ // Mock startViewTransition
137
+ document.startViewTransition = vi.fn((callback: () => void) => {
138
+ callback()
139
+ return {
140
+ finished: Promise.resolve(),
141
+ ready: Promise.resolve(),
142
+ types: new Set<string>(),
143
+ updateCallbackDone: Promise.resolve(),
144
+ skipTransition: () => {},
145
+ }
146
+ }) as any
147
+ })
148
+
149
+ it('handles android back press with view transition', async () => {
150
+ renderHook(() => useViewTransitions())
151
+
152
+ // Get the registered handler
153
+ const androidBackHandler = addEventListenerSpy.mock.calls.find(
154
+ call => call[0] === 'androidbackpressed'
155
+ )?.[1] as EventListener
156
+
157
+ expect(androidBackHandler).toBeDefined()
158
+
159
+ // Trigger android back press
160
+ await act(async () => {
161
+ androidBackHandler(new Event('androidbackpressed'))
162
+ })
163
+
164
+ expect(document.startViewTransition).toHaveBeenCalled()
165
+ expect(setAttributeSpy).toHaveBeenCalledWith(
166
+ 'data-navigation-type',
167
+ 'backward'
168
+ )
169
+ })
170
+
171
+ it('removes attribute after android back transition completes', async () => {
172
+ const transitionPromise = Promise.resolve()
173
+ const mockTransition = {
174
+ finished: transitionPromise,
175
+ ready: Promise.resolve(),
176
+ types: new Set<string>(),
177
+ updateCallbackDone: Promise.resolve(),
178
+ skipTransition: () => {},
179
+ }
180
+
181
+ document.startViewTransition = vi.fn((callback: () => void) => {
182
+ callback()
183
+ return mockTransition
184
+ }) as any
185
+
186
+ renderHook(() => useViewTransitions())
187
+
188
+ const androidBackHandler = addEventListenerSpy.mock.calls.find(
189
+ call => call[0] === 'androidbackpressed'
190
+ )?.[1] as EventListener
191
+
192
+ await act(async () => {
193
+ androidBackHandler(new Event('androidbackpressed'))
194
+ await transitionPromise
195
+ })
196
+
197
+ expect(removeAttributeSpy).toHaveBeenCalledWith('data-navigation-type')
198
+ })
199
+
200
+ it('handles view transition error on android back press', async () => {
201
+ const consoleErrorSpy = vi
202
+ .spyOn(console, 'error')
203
+ .mockImplementation(() => {})
204
+
205
+ const errorTransition = {
206
+ finished: Promise.reject(new Error('Transition failed')),
207
+ ready: Promise.resolve(),
208
+ types: new Set<string>(),
209
+ updateCallbackDone: Promise.resolve(),
210
+ skipTransition: () => {},
211
+ }
212
+
213
+ document.startViewTransition = vi.fn((callback: () => void) => {
214
+ callback()
215
+ return errorTransition
216
+ }) as any
217
+
218
+ renderHook(() => useViewTransitions())
219
+
220
+ const androidBackHandler = addEventListenerSpy.mock.calls.find(
221
+ call => call[0] === 'androidbackpressed'
222
+ )?.[1] as EventListener
223
+
224
+ await act(async () => {
225
+ androidBackHandler(new Event('androidbackpressed'))
226
+ try {
227
+ await errorTransition.finished
228
+ } catch {
229
+ // Expected error
230
+ }
231
+ })
232
+
233
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
234
+ 'View transition error:',
235
+ expect.any(Error)
236
+ )
237
+
238
+ consoleErrorSpy.mockRestore()
239
+ })
240
+
241
+ it('handles android back press without view transition support', () => {
242
+ // Remove startViewTransition
243
+ ;(document as any).startViewTransition = undefined
244
+
245
+ renderHook(() => useViewTransitions())
246
+
247
+ const androidBackHandler = addEventListenerSpy.mock.calls.find(
248
+ call => call[0] === 'androidbackpressed'
249
+ )?.[1] as EventListener
250
+
251
+ // Should not throw
252
+ expect(() => {
253
+ androidBackHandler(new Event('androidbackpressed'))
254
+ }).not.toThrow()
255
+ })
256
+ })
257
+
258
+ describe('Popstate Event', () => {
259
+ it('sets none navigation type for iOS back gesture', () => {
260
+ renderHook(() => useViewTransitions())
261
+
262
+ const popstateHandler = addEventListenerSpy.mock.calls.find(
263
+ call => call[0] === 'popstate'
264
+ )?.[1] as EventListener
265
+
266
+ expect(popstateHandler).toBeDefined()
267
+
268
+ // Create popstate event with hasUAVisualTransition
269
+ const popstateEvent = new PopStateEvent('popstate', {state: null})
270
+ ;(popstateEvent as any).hasUAVisualTransition = true
271
+
272
+ act(() => {
273
+ popstateHandler(popstateEvent)
274
+ })
275
+
276
+ expect(setAttributeSpy).toHaveBeenCalledWith(
277
+ 'data-navigation-type',
278
+ 'none'
279
+ )
280
+ })
281
+
282
+ it('does not set attribute for popstate without iOS gesture', () => {
283
+ renderHook(() => useViewTransitions())
284
+
285
+ const popstateHandler = addEventListenerSpy.mock.calls.find(
286
+ call => call[0] === 'popstate'
287
+ )?.[1] as EventListener
288
+
289
+ setAttributeSpy.mockClear()
290
+
291
+ // Regular popstate without hasUAVisualTransition
292
+ const popstateEvent = new PopStateEvent('popstate', {state: null})
293
+
294
+ act(() => {
295
+ popstateHandler(popstateEvent)
296
+ })
297
+
298
+ expect(setAttributeSpy).not.toHaveBeenCalledWith(
299
+ 'data-navigation-type',
300
+ 'none'
301
+ )
302
+ })
303
+
304
+ it('does not set none type after android back press', () => {
305
+ document.startViewTransition = vi.fn((callback: () => void) => {
306
+ callback()
307
+ return {
308
+ finished: Promise.resolve(),
309
+ ready: Promise.resolve(),
310
+ types: new Set<string>(),
311
+ updateCallbackDone: Promise.resolve(),
312
+ skipTransition: () => {},
313
+ }
314
+ }) as any
315
+
316
+ renderHook(() => useViewTransitions())
317
+
318
+ const androidBackHandler = addEventListenerSpy.mock.calls.find(
319
+ call => call[0] === 'androidbackpressed'
320
+ )?.[1] as EventListener
321
+
322
+ const popstateHandler = addEventListenerSpy.mock.calls.find(
323
+ call => call[0] === 'popstate'
324
+ )?.[1] as EventListener
325
+
326
+ // First trigger android back
327
+ act(() => {
328
+ androidBackHandler(new Event('androidbackpressed'))
329
+ })
330
+
331
+ setAttributeSpy.mockClear()
332
+
333
+ // Then trigger popstate with iOS gesture
334
+ const popstateEvent = new PopStateEvent('popstate', {state: null})
335
+ ;(popstateEvent as any).hasUAVisualTransition = true
336
+
337
+ act(() => {
338
+ popstateHandler(popstateEvent)
339
+ })
340
+
341
+ // Should not set none type because android back was pressed
342
+ expect(setAttributeSpy).not.toHaveBeenCalledWith(
343
+ 'data-navigation-type',
344
+ 'none'
345
+ )
346
+ })
347
+ })
348
+
349
+ describe('Navigation Type Changes', () => {
350
+ it('updates attribute when navigation type changes', () => {
351
+ mockNavigationType.mockReturnValue('PUSH')
352
+
353
+ const {rerender} = renderHook(() => useViewTransitions())
354
+
355
+ expect(setAttributeSpy).toHaveBeenCalledWith(
356
+ 'data-navigation-type',
357
+ 'forward'
358
+ )
359
+
360
+ setAttributeSpy.mockClear()
361
+
362
+ // Change navigation type
363
+ mockNavigationType.mockReturnValue('POP')
364
+ rerender()
365
+
366
+ expect(setAttributeSpy).toHaveBeenCalledWith(
367
+ 'data-navigation-type',
368
+ 'backward'
369
+ )
370
+ })
371
+
372
+ it('removes and re-adds attribute on navigation type change', () => {
373
+ mockNavigationType.mockReturnValue('PUSH')
374
+
375
+ const {rerender} = renderHook(() => useViewTransitions())
376
+
377
+ // Clear initial calls
378
+ removeAttributeSpy.mockClear()
379
+ setAttributeSpy.mockClear()
380
+
381
+ // Change navigation type
382
+ mockNavigationType.mockReturnValue('POP')
383
+ rerender()
384
+
385
+ // Should remove old attribute
386
+ expect(removeAttributeSpy).toHaveBeenCalledWith('data-navigation-type')
387
+ // And set new one
388
+ expect(setAttributeSpy).toHaveBeenCalledWith(
389
+ 'data-navigation-type',
390
+ 'backward'
391
+ )
392
+ })
393
+ })
394
+
395
+ describe('Edge Cases', () => {
396
+ it('handles multiple rapid location changes', () => {
397
+ const {rerender} = renderHook(() => useViewTransitions())
398
+
399
+ // Simulate rapid location changes
400
+ for (let i = 0; i < 5; i++) {
401
+ mockLocation.pathname = `/path-${i}`
402
+ rerender()
403
+ }
404
+
405
+ // Should still have proper listeners registered
406
+ const androidListeners = addEventListenerSpy.mock.calls.filter(
407
+ call => call[0] === 'androidbackpressed'
408
+ )
409
+ const popstateListeners = addEventListenerSpy.mock.calls.filter(
410
+ call => call[0] === 'popstate'
411
+ )
412
+
413
+ expect(androidListeners.length).toBeGreaterThan(0)
414
+ expect(popstateListeners.length).toBeGreaterThan(0)
415
+ })
416
+
417
+ it('handles missing startViewTransition gracefully', () => {
418
+ ;(document as any).startViewTransition = undefined
419
+
420
+ // Should not throw
421
+ expect(() => {
422
+ renderHook(() => useViewTransitions())
423
+ }).not.toThrow()
424
+ })
425
+
426
+ it('cleans up properly on unmount during transition', async () => {
427
+ let resolveTransition: (() => void) | undefined
428
+ const transitionPromise = new Promise<void>(resolve => {
429
+ resolveTransition = resolve
430
+ })
431
+
432
+ document.startViewTransition = vi.fn((callback: () => void) => {
433
+ callback()
434
+ return {
435
+ finished: transitionPromise,
436
+ ready: Promise.resolve(),
437
+ types: new Set<string>(),
438
+ updateCallbackDone: Promise.resolve(),
439
+ skipTransition: () => {},
440
+ }
441
+ }) as any
442
+
443
+ const {unmount} = renderHook(() => useViewTransitions())
444
+
445
+ const androidBackHandler = addEventListenerSpy.mock.calls.find(
446
+ call => call[0] === 'androidbackpressed'
447
+ )?.[1] as EventListener
448
+
449
+ // Start transition
450
+ act(() => {
451
+ androidBackHandler(new Event('androidbackpressed'))
452
+ })
453
+
454
+ // Unmount before transition completes
455
+ unmount()
456
+
457
+ // Complete transition after unmount
458
+ if (resolveTransition) {
459
+ resolveTransition()
460
+ }
461
+
462
+ // Should have removed listeners
463
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
464
+ 'androidbackpressed',
465
+ expect.any(Function)
466
+ )
467
+ })
468
+ })
469
+ })