@shopify/shop-minis-react 0.3.2 → 0.4.0

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 (205) hide show
  1. package/dist/components/MinisContainer.js +11 -10
  2. package/dist/components/MinisContainer.js.map +1 -1
  3. package/dist/components/atoms/content-wrapper.js.map +1 -1
  4. package/dist/components/atoms/video-player.js +28 -26
  5. package/dist/components/atoms/video-player.js.map +1 -1
  6. package/dist/components/commerce/product-card.js +106 -79
  7. package/dist/components/commerce/product-card.js.map +1 -1
  8. package/dist/components/commerce/product-link.js +124 -137
  9. package/dist/components/commerce/product-link.js.map +1 -1
  10. package/dist/components/commerce/search.js +20 -20
  11. package/dist/components/commerce/search.js.map +1 -1
  12. package/dist/components/ui/sonner.js +3 -1
  13. package/dist/components/ui/sonner.js.map +1 -1
  14. package/dist/hooks/content/useContent.js +12 -18
  15. package/dist/hooks/content/useContent.js.map +1 -1
  16. package/dist/hooks/navigation/useNavigateWithTransition.js +10 -11
  17. package/dist/hooks/navigation/useNavigateWithTransition.js.map +1 -1
  18. package/dist/hooks/product/useCuratedProducts.js +9 -11
  19. package/dist/hooks/product/useCuratedProducts.js.map +1 -1
  20. package/dist/hooks/product/usePopularProducts.js +9 -11
  21. package/dist/hooks/product/usePopularProducts.js.map +1 -1
  22. package/dist/hooks/product/useProduct.js +11 -17
  23. package/dist/hooks/product/useProduct.js.map +1 -1
  24. package/dist/hooks/product/useProductList.js +10 -21
  25. package/dist/hooks/product/useProductList.js.map +1 -1
  26. package/dist/hooks/product/useProductLists.js +11 -13
  27. package/dist/hooks/product/useProductLists.js.map +1 -1
  28. package/dist/hooks/product/useProductMedia.js +12 -18
  29. package/dist/hooks/product/useProductMedia.js.map +1 -1
  30. package/dist/hooks/product/useProductSearch.js +34 -27
  31. package/dist/hooks/product/useProductSearch.js.map +1 -1
  32. package/dist/hooks/product/useProductVariants.js +11 -14
  33. package/dist/hooks/product/useProductVariants.js.map +1 -1
  34. package/dist/hooks/product/useProducts.js +12 -11
  35. package/dist/hooks/product/useProducts.js.map +1 -1
  36. package/dist/hooks/product/useRecommendedProducts.js +11 -13
  37. package/dist/hooks/product/useRecommendedProducts.js.map +1 -1
  38. package/dist/hooks/shop/useRecommendedShops.js +11 -13
  39. package/dist/hooks/shop/useRecommendedShops.js.map +1 -1
  40. package/dist/hooks/shop/useShop.js +12 -11
  41. package/dist/hooks/shop/useShop.js.map +1 -1
  42. package/dist/hooks/user/useBuyerAttributes.js +8 -10
  43. package/dist/hooks/user/useBuyerAttributes.js.map +1 -1
  44. package/dist/hooks/user/useCurrentUser.js +7 -9
  45. package/dist/hooks/user/useCurrentUser.js.map +1 -1
  46. package/dist/hooks/user/useFollowedShops.js +11 -14
  47. package/dist/hooks/user/useFollowedShops.js.map +1 -1
  48. package/dist/hooks/user/useOrders.js +7 -9
  49. package/dist/hooks/user/useOrders.js.map +1 -1
  50. package/dist/hooks/user/useRecentProducts.js +11 -13
  51. package/dist/hooks/user/useRecentProducts.js.map +1 -1
  52. package/dist/hooks/user/useRecentShops.js +10 -13
  53. package/dist/hooks/user/useRecentShops.js.map +1 -1
  54. package/dist/hooks/user/useSavedProducts.js +10 -13
  55. package/dist/hooks/user/useSavedProducts.js.map +1 -1
  56. package/dist/index.js +269 -264
  57. package/dist/index.js.map +1 -1
  58. package/dist/internal/components/product-review-stars.js +78 -0
  59. package/dist/internal/components/product-review-stars.js.map +1 -0
  60. package/dist/internal/reactQuery/MinisQueryProvider.js +11 -0
  61. package/dist/internal/reactQuery/MinisQueryProvider.js.map +1 -0
  62. package/dist/internal/reactQuery/queryClient.js +33 -0
  63. package/dist/internal/reactQuery/queryClient.js.map +1 -0
  64. package/dist/internal/reactQuery/useShopActionInfiniteQuery.js +52 -0
  65. package/dist/internal/reactQuery/useShopActionInfiniteQuery.js.map +1 -0
  66. package/dist/internal/reactQuery/useShopActionQuery.js +37 -0
  67. package/dist/internal/reactQuery/useShopActionQuery.js.map +1 -0
  68. package/dist/mocks.js +178 -107
  69. package/dist/mocks.js.map +1 -1
  70. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/focusManager.js +45 -0
  71. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/focusManager.js.map +1 -0
  72. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/infiniteQueryBehavior.js +89 -0
  73. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/infiniteQueryBehavior.js.map +1 -0
  74. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/infiniteQueryObserver.js +55 -0
  75. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/infiniteQueryObserver.js.map +1 -0
  76. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/mutation.js +198 -0
  77. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/mutation.js.map +1 -0
  78. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/mutationCache.js +99 -0
  79. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/mutationCache.js.map +1 -0
  80. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/notifyManager.js +67 -0
  81. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/notifyManager.js.map +1 -0
  82. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/onlineManager.js +39 -0
  83. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/onlineManager.js.map +1 -0
  84. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/query.js +299 -0
  85. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/query.js.map +1 -0
  86. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryCache.js +80 -0
  87. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryCache.js.map +1 -0
  88. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryClient.js +215 -0
  89. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryClient.js.map +1 -0
  90. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryObserver.js +300 -0
  91. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryObserver.js.map +1 -0
  92. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/removable.js +25 -0
  93. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/removable.js.map +1 -0
  94. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/retryer.js +76 -0
  95. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/retryer.js.map +1 -0
  96. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/subscribable.js +21 -0
  97. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/subscribable.js.map +1 -0
  98. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/thenable.js +26 -0
  99. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/thenable.js.map +1 -0
  100. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/utils.js +176 -0
  101. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/utils.js.map +1 -0
  102. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/IsRestoringProvider.js +7 -0
  103. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/IsRestoringProvider.js.map +1 -0
  104. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/QueryClientProvider.js +17 -0
  105. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/QueryClientProvider.js.map +1 -0
  106. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/QueryErrorResetBoundary.js +19 -0
  107. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/QueryErrorResetBoundary.js.map +1 -0
  108. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/errorBoundaryUtils.js +21 -0
  109. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/errorBoundaryUtils.js.map +1 -0
  110. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/suspense.js +18 -0
  111. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/suspense.js.map +1 -0
  112. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useBaseQuery.js +64 -0
  113. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useBaseQuery.js.map +1 -0
  114. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useInfiniteQuery.js +13 -0
  115. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useInfiniteQuery.js.map +1 -0
  116. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useQuery.js +9 -0
  117. package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useQuery.js.map +1 -0
  118. package/dist/shop-minis-react/node_modules/.pnpm/lucide-react@0.513.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/star-half.js +21 -0
  119. package/dist/shop-minis-react/node_modules/.pnpm/lucide-react@0.513.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/star-half.js.map +1 -0
  120. package/dist/shop-minis-react/node_modules/.pnpm/sonner@2.0.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/sonner/dist/index.js +4 -4
  121. package/dist/shop-minis-react/node_modules/.pnpm/sonner@2.0.5_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/sonner/dist/index.js.map +1 -1
  122. package/package.json +2 -7
  123. package/src/components/MinisContainer.tsx +6 -3
  124. package/src/components/atoms/content-wrapper.tsx +1 -1
  125. package/src/components/atoms/video-player.tsx +7 -0
  126. package/src/components/commerce/product-card.test.tsx +135 -0
  127. package/src/components/commerce/product-card.tsx +39 -5
  128. package/src/components/commerce/product-link.test.tsx +15 -3
  129. package/src/components/commerce/product-link.tsx +9 -25
  130. package/src/components/commerce/search.tsx +2 -2
  131. package/src/components/index.ts +1 -0
  132. package/src/components/ui/sonner.tsx +2 -2
  133. package/src/hooks/content/useContent.ts +6 -17
  134. package/src/hooks/navigation/useNavigateWithTransition.test.ts +46 -7
  135. package/src/hooks/navigation/useNavigateWithTransition.ts +4 -1
  136. package/src/hooks/product/useCuratedProducts.ts +4 -6
  137. package/src/hooks/product/usePopularProducts.ts +4 -6
  138. package/src/hooks/product/useProduct.ts +6 -17
  139. package/src/hooks/product/useProductList.ts +4 -19
  140. package/src/hooks/product/useProductLists.ts +4 -6
  141. package/src/hooks/product/useProductMedia.ts +6 -17
  142. package/src/hooks/product/useProductSearch.ts +19 -15
  143. package/src/hooks/product/useProductVariants.ts +5 -13
  144. package/src/hooks/product/useProducts.ts +8 -12
  145. package/src/hooks/product/useRecommendedProducts.ts +4 -6
  146. package/src/hooks/shop/useRecommendedShops.ts +4 -6
  147. package/src/hooks/shop/useShop.ts +8 -12
  148. package/src/hooks/user/useBuyerAttributes.ts +4 -6
  149. package/src/hooks/user/useCurrentUser.ts +4 -6
  150. package/src/hooks/user/useFollowedShops.ts +5 -13
  151. package/src/hooks/user/useOrders.ts +4 -6
  152. package/src/hooks/user/useRecentProducts.ts +4 -6
  153. package/src/hooks/user/useRecentShops.ts +5 -13
  154. package/src/hooks/user/useSavedProducts.ts +5 -13
  155. package/src/internal/components/product-review-stars.test.tsx +90 -0
  156. package/src/internal/components/product-review-stars.tsx +113 -0
  157. package/src/internal/reactQuery/MinisQueryProvider.test.tsx +38 -0
  158. package/src/internal/reactQuery/MinisQueryProvider.tsx +16 -0
  159. package/src/internal/reactQuery/index.ts +8 -0
  160. package/src/internal/reactQuery/queryClient.test.tsx +91 -0
  161. package/src/internal/reactQuery/queryClient.ts +43 -0
  162. package/src/internal/reactQuery/useShopActionInfiniteQuery.test.tsx +357 -0
  163. package/src/internal/reactQuery/useShopActionInfiniteQuery.ts +129 -0
  164. package/src/internal/reactQuery/useShopActionQuery.test.tsx +184 -0
  165. package/src/internal/reactQuery/useShopActionQuery.ts +74 -0
  166. package/src/mocks.ts +10 -2
  167. package/src/providers/ImagePickerProvider.test.tsx +3 -9
  168. package/dist/internal/useShopActionsDataFetching.js +0 -79
  169. package/dist/internal/useShopActionsDataFetching.js.map +0 -1
  170. package/dist/internal/useShopActionsPaginatedDataFetching.js +0 -96
  171. package/dist/internal/useShopActionsPaginatedDataFetching.js.map +0 -1
  172. package/src/hooks/product/useProductSearch.test.ts +0 -470
  173. package/src/internal/useShopActionsDataFetching.test.ts +0 -465
  174. package/src/internal/useShopActionsDataFetching.ts +0 -150
  175. package/src/internal/useShopActionsPaginatedDataFetching.ts +0 -188
  176. package/src/stories/Accordion.stories.tsx +0 -124
  177. package/src/stories/AddToCart.stories.tsx +0 -251
  178. package/src/stories/Alert.stories.tsx +0 -38
  179. package/src/stories/AlertDialog.stories.tsx +0 -48
  180. package/src/stories/Avatar.stories.tsx +0 -29
  181. package/src/stories/Badge.stories.tsx +0 -46
  182. package/src/stories/Button.stories.tsx +0 -81
  183. package/src/stories/Card.stories.tsx +0 -40
  184. package/src/stories/Checkbox.stories.tsx +0 -44
  185. package/src/stories/FavoriteButton.stories.tsx +0 -58
  186. package/src/stories/IconButton.stories.tsx +0 -68
  187. package/src/stories/ImageContentWrapper.stories.tsx +0 -65
  188. package/src/stories/Input.stories.tsx +0 -44
  189. package/src/stories/Label.stories.tsx +0 -19
  190. package/src/stories/List.stories.tsx +0 -64
  191. package/src/stories/MerchantCard.stories.tsx +0 -127
  192. package/src/stories/ProductCard.stories.tsx +0 -92
  193. package/src/stories/ProductLink.stories.tsx +0 -46
  194. package/src/stories/ProductVariantPrice.stories.tsx +0 -70
  195. package/src/stories/Progress.stories.tsx +0 -30
  196. package/src/stories/PullToRefreshList.stories.tsx +0 -122
  197. package/src/stories/QuantitySelector.stories.tsx +0 -78
  198. package/src/stories/RadioGroup.stories.tsx +0 -51
  199. package/src/stories/Search.stories.tsx +0 -37
  200. package/src/stories/Select.stories.tsx +0 -85
  201. package/src/stories/Skeleton.stories.tsx +0 -19
  202. package/src/stories/TextInput.stories.tsx +0 -26
  203. package/src/stories/Toaster.stories.tsx +0 -46
  204. package/src/stories/Touchable.stories.tsx +0 -40
  205. package/src/stories/VideoPlayer.stories.tsx +0 -129
@@ -1,465 +0,0 @@
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
- })
@@ -1,150 +0,0 @@
1
- import {useCallback, useEffect, useMemo, useState} from 'react'
2
-
3
- import {ShopActionResult} from '@shopify/shop-minis-platform/actions'
4
-
5
- import {DataHookFetchPolicy, DataHookReturnsBase} from '../types'
6
- import {formatError, MiniError} from '../utils/errors'
7
-
8
- export interface ShopActionsDataFetchingResult<R> extends DataHookReturnsBase {
9
- data: R | null
10
- }
11
-
12
- export const useShopActionsDataFetching = <
13
- S = unknown,
14
- P extends {fetchPolicy?: DataHookFetchPolicy} = {
15
- fetchPolicy?: DataHookFetchPolicy
16
- },
17
- >(
18
- action: (params: P) => Promise<ShopActionResult<{data: S}>>,
19
- params: P,
20
- options: {
21
- skip?: boolean
22
- hook?: string
23
- validator?: (data: S) => void
24
- }
25
- ): ShopActionsDataFetchingResult<S> => {
26
- const [state, setState] = useState<{
27
- data: S | null
28
- loading: boolean
29
- error: Error | null
30
- }>({
31
- data: null,
32
- loading: true,
33
- error: null,
34
- })
35
-
36
- const skip = options?.skip === true
37
- const {validator, hook} = options
38
- const runValidator = useCallback(
39
- (dataToValidate: S) => {
40
- try {
41
- validator?.(dataToValidate)
42
- return null
43
- } catch (err) {
44
- if (err instanceof Error) return err
45
-
46
- return new MiniError({
47
- hook,
48
- message: 'Validation failed',
49
- })
50
- }
51
- },
52
- [validator, hook]
53
- )
54
-
55
- // Params object is recreated on every render, so we need to memoize it.
56
- // We don't know what's inside the params object, but we can stringify it.
57
- // eslint-disable-next-line react-hooks/exhaustive-deps
58
- const stableParams = useMemo(() => params, [JSON.stringify(params)])
59
-
60
- // There's a lot of complexity here because each type of fetch has different side effects if we are trying to
61
- // stay close to how Apollo client works. eg:
62
- // - Initial fetch: set loading, set error, set data, reset on error (don't throw)
63
- // - change params fetch: set loading, set error, set data, reset on error (don't throw)
64
- // - refetch fetch: don't set loading, set error, update data, leave data as is was on error (also throw)
65
- // - fetchMore fetch: don't set loading, don't set error, update data, leave data as is was on error (also throw)
66
- const fetch = useCallback(
67
- async (
68
- extraParams?: Partial<P>,
69
- {
70
- setLoading = true,
71
- setError = true,
72
- resetOnError = true,
73
- throwOnError = true,
74
- }: {
75
- setLoading?: boolean
76
- setError?: boolean
77
- resetOnError?: boolean
78
- throwOnError?: boolean
79
- } = {}
80
- ) => {
81
- let queryError: Error | null = null
82
- let validationError: Error | null = null
83
-
84
- setState(curState => ({
85
- ...curState,
86
- loading: setLoading ? true : curState.loading,
87
- }))
88
-
89
- try {
90
- const result = await action({...stableParams, ...extraParams})
91
-
92
- if (result.ok) {
93
- validationError = runValidator(result.data.data)
94
-
95
- setState(curState => ({
96
- ...curState,
97
- data: result.data.data,
98
- loading: false,
99
- error: validationError ?? null,
100
- }))
101
- } else {
102
- throw result.error
103
- }
104
- } catch (err) {
105
- queryError = formatError({hook}, err)
106
- }
107
-
108
- const error = validationError || queryError
109
-
110
- if (error && (setError || resetOnError)) {
111
- setState(curState => ({
112
- data: resetOnError ? null : curState.data,
113
- loading: false,
114
- error,
115
- }))
116
- }
117
-
118
- if (error && throwOnError) {
119
- throw error
120
- }
121
- },
122
- [action, stableParams, hook, runValidator]
123
- )
124
-
125
- const refetch = useCallback(async () => {
126
- await fetch({fetchPolicy: 'network-only'} as Partial<P>, {
127
- setLoading: false,
128
- resetOnError: false,
129
- throwOnError: true,
130
- })
131
- }, [fetch])
132
-
133
- useEffect(() => {
134
- if (skip) return
135
-
136
- fetch(
137
- {},
138
- {
139
- throwOnError: false,
140
- }
141
- )
142
- }, [fetch, skip])
143
-
144
- return {
145
- data: state.data,
146
- loading: state.loading,
147
- error: state.error,
148
- refetch,
149
- }
150
- }