@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,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,
|