@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,470 @@
|
|
|
1
|
+
import {renderHook, act, waitFor} from '@testing-library/react'
|
|
2
|
+
import {describe, expect, it, vi, beforeEach} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {useShopActions} from '../../internal/useShopActions'
|
|
5
|
+
import {useShopActionsPaginatedDataFetching} from '../../internal/useShopActionsPaginatedDataFetching'
|
|
6
|
+
import {mockProducts} from '../../test-utils'
|
|
7
|
+
|
|
8
|
+
import {useProductSearch} from './useProductSearch'
|
|
9
|
+
|
|
10
|
+
// Mock lodash debounce to make tests predictable
|
|
11
|
+
vi.mock('lodash/debounce', () => ({
|
|
12
|
+
default: (fn: any) => {
|
|
13
|
+
const debounced = (...args: any[]) => fn(...args)
|
|
14
|
+
debounced.cancel = vi.fn()
|
|
15
|
+
return debounced
|
|
16
|
+
},
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
// Mock dependencies
|
|
20
|
+
vi.mock('../../internal/useShopActions', () => ({
|
|
21
|
+
useShopActions: vi.fn(() => ({
|
|
22
|
+
getProductSearch: vi.fn(),
|
|
23
|
+
})),
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
vi.mock('../../internal/useShopActionsPaginatedDataFetching', () => ({
|
|
27
|
+
useShopActionsPaginatedDataFetching: vi.fn(),
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
describe('useProductSearch', () => {
|
|
31
|
+
let mockGetProductSearch: ReturnType<typeof vi.fn>
|
|
32
|
+
let mockPaginatedDataFetching: any
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks()
|
|
36
|
+
|
|
37
|
+
mockGetProductSearch = vi.fn()
|
|
38
|
+
|
|
39
|
+
// Default mock for paginated data fetching
|
|
40
|
+
mockPaginatedDataFetching = {
|
|
41
|
+
data: mockProducts(3),
|
|
42
|
+
loading: false,
|
|
43
|
+
error: null,
|
|
44
|
+
hasNextPage: false,
|
|
45
|
+
fetchMore: vi.fn(),
|
|
46
|
+
refetch: vi.fn(),
|
|
47
|
+
}
|
|
48
|
+
;(useShopActions as any).mockReturnValue({
|
|
49
|
+
getProductSearch: mockGetProductSearch,
|
|
50
|
+
})
|
|
51
|
+
;(useShopActionsPaginatedDataFetching as any).mockReturnValue(
|
|
52
|
+
mockPaginatedDataFetching
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('Basic Search', () => {
|
|
57
|
+
it('returns products when query is provided', () => {
|
|
58
|
+
const {result} = renderHook(() =>
|
|
59
|
+
useProductSearch({
|
|
60
|
+
query: 'test query',
|
|
61
|
+
})
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
expect(result.current.products).toEqual(mockProducts(3))
|
|
65
|
+
expect(result.current.loading).toBe(false)
|
|
66
|
+
expect(result.current.error).toBe(null)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('returns null products when query is empty', () => {
|
|
70
|
+
const {result} = renderHook(() =>
|
|
71
|
+
useProductSearch({
|
|
72
|
+
query: '',
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
expect(result.current.products).toBe(null)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('returns null products when query contains only whitespace', () => {
|
|
80
|
+
const {result} = renderHook(() =>
|
|
81
|
+
useProductSearch({
|
|
82
|
+
query: ' ',
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
expect(result.current.products).toBe(null)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('passes search parameters correctly', () => {
|
|
90
|
+
renderHook(() =>
|
|
91
|
+
useProductSearch({
|
|
92
|
+
query: 'test',
|
|
93
|
+
filters: {} as any, // Empty filters object for testing
|
|
94
|
+
sortBy: 'RELEVANCE' as any,
|
|
95
|
+
includeSensitive: true,
|
|
96
|
+
})
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
expect(useShopActionsPaginatedDataFetching).toHaveBeenCalledWith(
|
|
100
|
+
mockGetProductSearch,
|
|
101
|
+
expect.objectContaining({
|
|
102
|
+
query: 'test',
|
|
103
|
+
filters: {},
|
|
104
|
+
sortBy: 'RELEVANCE',
|
|
105
|
+
includeSensitive: true,
|
|
106
|
+
}),
|
|
107
|
+
expect.objectContaining({
|
|
108
|
+
skip: false,
|
|
109
|
+
hook: 'useProductSearch',
|
|
110
|
+
})
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('handles skip parameter', () => {
|
|
115
|
+
renderHook(() =>
|
|
116
|
+
useProductSearch({
|
|
117
|
+
query: 'test',
|
|
118
|
+
skip: true,
|
|
119
|
+
})
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
expect(useShopActionsPaginatedDataFetching).toHaveBeenCalledWith(
|
|
123
|
+
expect.any(Function),
|
|
124
|
+
expect.any(Object),
|
|
125
|
+
expect.objectContaining({
|
|
126
|
+
skip: true,
|
|
127
|
+
})
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('Debouncing', () => {
|
|
133
|
+
it('debounces query changes', async () => {
|
|
134
|
+
// Create a custom mock for debounce that we can control
|
|
135
|
+
let debouncedCallback: any = null
|
|
136
|
+
const mockDebounce = vi.fn((fn: any, _delay: number) => {
|
|
137
|
+
const debounced = (...args: any[]) => {
|
|
138
|
+
debouncedCallback = () => fn(...args)
|
|
139
|
+
}
|
|
140
|
+
debounced.cancel = vi.fn()
|
|
141
|
+
return debounced
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// Mock lodash/debounce with our custom implementation
|
|
145
|
+
vi.doMock('lodash/debounce', () => ({
|
|
146
|
+
default: mockDebounce,
|
|
147
|
+
}))
|
|
148
|
+
|
|
149
|
+
// Clear module cache and reimport with mocked debounce
|
|
150
|
+
vi.resetModules()
|
|
151
|
+
const {useProductSearch: mockedUseProductSearch} = await import(
|
|
152
|
+
'./useProductSearch'
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
const {result, rerender} = renderHook(
|
|
156
|
+
({query}) => mockedUseProductSearch({query}),
|
|
157
|
+
{
|
|
158
|
+
initialProps: {query: 'initial'},
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
// Initially not typing
|
|
163
|
+
expect(result.current.isTyping).toBe(false)
|
|
164
|
+
|
|
165
|
+
// Change query
|
|
166
|
+
rerender({query: 'updated'})
|
|
167
|
+
|
|
168
|
+
// isTyping should be true immediately after query change
|
|
169
|
+
expect(result.current.isTyping).toBe(true)
|
|
170
|
+
|
|
171
|
+
// Simulate debounce callback execution
|
|
172
|
+
await act(async () => {
|
|
173
|
+
if (debouncedCallback) {
|
|
174
|
+
debouncedCallback()
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// After debounce executes, isTyping should be false
|
|
179
|
+
await waitFor(() => {
|
|
180
|
+
expect(result.current.isTyping).toBe(false)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// Restore original mock
|
|
184
|
+
vi.doMock('lodash/debounce', () => ({
|
|
185
|
+
default: (fn: any) => {
|
|
186
|
+
const debounced = (...args: any[]) => fn(...args)
|
|
187
|
+
debounced.cancel = vi.fn()
|
|
188
|
+
return debounced
|
|
189
|
+
},
|
|
190
|
+
}))
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('cancels debounced function on unmount', async () => {
|
|
194
|
+
const cancelSpy = vi.fn()
|
|
195
|
+
|
|
196
|
+
// Mock debounce with cancel spy
|
|
197
|
+
vi.doMock('lodash/debounce', () => ({
|
|
198
|
+
default: (fn: any) => {
|
|
199
|
+
const debounced = (...args: any[]) => fn(...args)
|
|
200
|
+
debounced.cancel = cancelSpy
|
|
201
|
+
return debounced
|
|
202
|
+
},
|
|
203
|
+
}))
|
|
204
|
+
|
|
205
|
+
// Clear module cache and reimport with mocked debounce
|
|
206
|
+
vi.resetModules()
|
|
207
|
+
const {useProductSearch: mockedUseProductSearch} = await import(
|
|
208
|
+
'./useProductSearch'
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
const {unmount} = renderHook(() =>
|
|
212
|
+
mockedUseProductSearch({
|
|
213
|
+
query: 'test',
|
|
214
|
+
})
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
unmount()
|
|
218
|
+
|
|
219
|
+
expect(cancelSpy).toHaveBeenCalled()
|
|
220
|
+
|
|
221
|
+
// Restore original mock
|
|
222
|
+
vi.doMock('lodash/debounce', () => ({
|
|
223
|
+
default: (fn: any) => {
|
|
224
|
+
const debounced = (...args: any[]) => fn(...args)
|
|
225
|
+
debounced.cancel = vi.fn()
|
|
226
|
+
return debounced
|
|
227
|
+
},
|
|
228
|
+
}))
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe('isTyping State', () => {
|
|
233
|
+
it('returns isTyping as false when debounced query matches current query', () => {
|
|
234
|
+
const {result} = renderHook(() =>
|
|
235
|
+
useProductSearch({
|
|
236
|
+
query: 'test',
|
|
237
|
+
})
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
expect(result.current.isTyping).toBe(false)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('returns isTyping as true when queries differ', async () => {
|
|
244
|
+
// Create a custom mock for debounce that we can control
|
|
245
|
+
let debouncedCallback: any = null
|
|
246
|
+
const mockDebounce = vi.fn((fn: any, _delay: number) => {
|
|
247
|
+
const debounced = (...args: any[]) => {
|
|
248
|
+
debouncedCallback = () => fn(...args)
|
|
249
|
+
}
|
|
250
|
+
debounced.cancel = vi.fn()
|
|
251
|
+
return debounced
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// Mock lodash/debounce with our custom implementation
|
|
255
|
+
vi.doMock('lodash/debounce', () => ({
|
|
256
|
+
default: mockDebounce,
|
|
257
|
+
}))
|
|
258
|
+
|
|
259
|
+
// Clear module cache and reimport with mocked debounce
|
|
260
|
+
vi.resetModules()
|
|
261
|
+
const {useProductSearch: mockedUseProductSearch} = await import(
|
|
262
|
+
'./useProductSearch'
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
const {result, rerender} = renderHook(
|
|
266
|
+
({query}) => mockedUseProductSearch({query}),
|
|
267
|
+
{
|
|
268
|
+
initialProps: {query: 'initial'},
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
// Initially not typing
|
|
273
|
+
expect(result.current.isTyping).toBe(false)
|
|
274
|
+
|
|
275
|
+
// Update query
|
|
276
|
+
rerender({query: 'new query'})
|
|
277
|
+
|
|
278
|
+
// Should be typing now
|
|
279
|
+
expect(result.current.isTyping).toBe(true)
|
|
280
|
+
|
|
281
|
+
// Simulate debounce callback execution
|
|
282
|
+
await act(async () => {
|
|
283
|
+
if (debouncedCallback) {
|
|
284
|
+
debouncedCallback()
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
// Should no longer be typing after debounce executes
|
|
289
|
+
await waitFor(() => {
|
|
290
|
+
expect(result.current.isTyping).toBe(false)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// Restore original mock
|
|
294
|
+
vi.doMock('lodash/debounce', () => ({
|
|
295
|
+
default: (fn: any) => {
|
|
296
|
+
const debounced = (...args: any[]) => fn(...args)
|
|
297
|
+
debounced.cancel = vi.fn()
|
|
298
|
+
return debounced
|
|
299
|
+
},
|
|
300
|
+
}))
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
describe('Pagination', () => {
|
|
305
|
+
it('exposes pagination methods from underlying hook', () => {
|
|
306
|
+
const mockFetchMore = vi.fn()
|
|
307
|
+
const mockRefetch = vi.fn()
|
|
308
|
+
|
|
309
|
+
;(useShopActionsPaginatedDataFetching as any).mockReturnValue({
|
|
310
|
+
data: mockProducts(3),
|
|
311
|
+
loading: false,
|
|
312
|
+
error: null,
|
|
313
|
+
hasNextPage: true,
|
|
314
|
+
fetchMore: mockFetchMore,
|
|
315
|
+
refetch: mockRefetch,
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
const {result} = renderHook(() =>
|
|
319
|
+
useProductSearch({
|
|
320
|
+
query: 'test',
|
|
321
|
+
})
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
expect(result.current.hasNextPage).toBe(true)
|
|
325
|
+
expect(result.current.fetchMore).toBe(mockFetchMore)
|
|
326
|
+
expect(result.current.refetch).toBe(mockRefetch)
|
|
327
|
+
|
|
328
|
+
// Call the methods to ensure they're properly exposed
|
|
329
|
+
result.current.fetchMore()
|
|
330
|
+
result.current.refetch()
|
|
331
|
+
|
|
332
|
+
expect(mockFetchMore).toHaveBeenCalledTimes(1)
|
|
333
|
+
expect(mockRefetch).toHaveBeenCalledTimes(1)
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('Loading and Error States', () => {
|
|
338
|
+
it('passes through loading state', () => {
|
|
339
|
+
;(useShopActionsPaginatedDataFetching as any).mockReturnValue({
|
|
340
|
+
data: null,
|
|
341
|
+
loading: true,
|
|
342
|
+
error: null,
|
|
343
|
+
hasNextPage: false,
|
|
344
|
+
fetchMore: vi.fn(),
|
|
345
|
+
refetch: vi.fn(),
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const {result} = renderHook(() =>
|
|
349
|
+
useProductSearch({
|
|
350
|
+
query: 'test',
|
|
351
|
+
})
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
expect(result.current.loading).toBe(true)
|
|
355
|
+
expect(result.current.products).toBe(null)
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('passes through error state', () => {
|
|
359
|
+
const testError = new Error('Search failed')
|
|
360
|
+
|
|
361
|
+
;(useShopActionsPaginatedDataFetching as any).mockReturnValue({
|
|
362
|
+
data: null,
|
|
363
|
+
loading: false,
|
|
364
|
+
error: testError,
|
|
365
|
+
hasNextPage: false,
|
|
366
|
+
fetchMore: vi.fn(),
|
|
367
|
+
refetch: vi.fn(),
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
const {result} = renderHook(() =>
|
|
371
|
+
useProductSearch({
|
|
372
|
+
query: 'test',
|
|
373
|
+
})
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
expect(result.current.error).toBe(testError)
|
|
377
|
+
expect(result.current.products).toBe(null)
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
describe('Filters and Sorting', () => {
|
|
382
|
+
it('passes filters to underlying hook', () => {
|
|
383
|
+
const filters = {} as any // Mock filters object
|
|
384
|
+
|
|
385
|
+
renderHook(() =>
|
|
386
|
+
useProductSearch({
|
|
387
|
+
query: 'test',
|
|
388
|
+
filters,
|
|
389
|
+
})
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
expect(useShopActionsPaginatedDataFetching).toHaveBeenCalledWith(
|
|
393
|
+
expect.any(Function),
|
|
394
|
+
expect.objectContaining({
|
|
395
|
+
filters,
|
|
396
|
+
}),
|
|
397
|
+
expect.any(Object)
|
|
398
|
+
)
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('passes sortBy to underlying hook', () => {
|
|
402
|
+
renderHook(() =>
|
|
403
|
+
useProductSearch({
|
|
404
|
+
query: 'test',
|
|
405
|
+
sortBy: 'RELEVANCE',
|
|
406
|
+
})
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
expect(useShopActionsPaginatedDataFetching).toHaveBeenCalledWith(
|
|
410
|
+
expect.any(Function),
|
|
411
|
+
expect.objectContaining({
|
|
412
|
+
sortBy: 'RELEVANCE',
|
|
413
|
+
}),
|
|
414
|
+
expect.any(Object)
|
|
415
|
+
)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('uses default value for includeSensitive', () => {
|
|
419
|
+
renderHook(() =>
|
|
420
|
+
useProductSearch({
|
|
421
|
+
query: 'test',
|
|
422
|
+
})
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
expect(useShopActionsPaginatedDataFetching).toHaveBeenCalledWith(
|
|
426
|
+
expect.any(Function),
|
|
427
|
+
expect.objectContaining({
|
|
428
|
+
includeSensitive: false,
|
|
429
|
+
}),
|
|
430
|
+
expect.any(Object)
|
|
431
|
+
)
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
describe('Hook Options', () => {
|
|
436
|
+
it('passes pagination parameters', () => {
|
|
437
|
+
renderHook(
|
|
438
|
+
() =>
|
|
439
|
+
useProductSearch({
|
|
440
|
+
query: 'test',
|
|
441
|
+
first: 20,
|
|
442
|
+
} as any) // Type assertion to avoid type error
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
expect(useShopActionsPaginatedDataFetching).toHaveBeenCalledWith(
|
|
446
|
+
expect.any(Function),
|
|
447
|
+
expect.objectContaining({
|
|
448
|
+
first: 20,
|
|
449
|
+
}),
|
|
450
|
+
expect.any(Object)
|
|
451
|
+
)
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
it('provides hook name for error tracking', () => {
|
|
455
|
+
renderHook(() =>
|
|
456
|
+
useProductSearch({
|
|
457
|
+
query: 'test',
|
|
458
|
+
})
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
expect(useShopActionsPaginatedDataFetching).toHaveBeenCalledWith(
|
|
462
|
+
expect.any(Function),
|
|
463
|
+
expect.any(Object),
|
|
464
|
+
expect.objectContaining({
|
|
465
|
+
hook: 'useProductSearch',
|
|
466
|
+
})
|
|
467
|
+
)
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
})
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import {renderHook, act} from '@testing-library/react'
|
|
2
|
+
import {describe, expect, it, vi, beforeEach} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {useHandleAction} from '../../internal/useHandleAction'
|
|
5
|
+
import {useShopActions} from '../../internal/useShopActions'
|
|
6
|
+
|
|
7
|
+
import {useAsyncStorage} from './useAsyncStorage'
|
|
8
|
+
|
|
9
|
+
// Mock the internal hooks
|
|
10
|
+
vi.mock('../../internal/useShopActions', () => ({
|
|
11
|
+
useShopActions: vi.fn(() => ({
|
|
12
|
+
getPersistedItem: vi.fn(),
|
|
13
|
+
setPersistedItem: vi.fn(),
|
|
14
|
+
removePersistedItem: vi.fn(),
|
|
15
|
+
getAllPersistedKeys: vi.fn(),
|
|
16
|
+
clearPersistedItems: vi.fn(),
|
|
17
|
+
})),
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
vi.mock('../../internal/useHandleAction', () => ({
|
|
21
|
+
useHandleAction: vi.fn((action: any) => action),
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
describe('useAsyncStorage', () => {
|
|
25
|
+
let mockActions: {[key: string]: ReturnType<typeof vi.fn>}
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.clearAllMocks()
|
|
29
|
+
|
|
30
|
+
// Set up mock actions with proper implementations
|
|
31
|
+
mockActions = {
|
|
32
|
+
getPersistedItem: vi.fn().mockResolvedValue('stored-value'),
|
|
33
|
+
setPersistedItem: vi.fn().mockResolvedValue(undefined),
|
|
34
|
+
removePersistedItem: vi.fn().mockResolvedValue(undefined),
|
|
35
|
+
getAllPersistedKeys: vi.fn().mockResolvedValue(['key1', 'key2', 'key3']),
|
|
36
|
+
clearPersistedItems: vi.fn().mockResolvedValue(undefined),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Update the mock to return our mock actions
|
|
40
|
+
;(useShopActions as ReturnType<typeof vi.fn>).mockReturnValue(mockActions)
|
|
41
|
+
|
|
42
|
+
// Make useHandleAction return the action directly
|
|
43
|
+
;(useHandleAction as ReturnType<typeof vi.fn>).mockImplementation(
|
|
44
|
+
(action: any) => action
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('Hook Structure', () => {
|
|
49
|
+
it('returns all expected methods', () => {
|
|
50
|
+
const {result} = renderHook(() => useAsyncStorage())
|
|
51
|
+
|
|
52
|
+
expect(result.current).toHaveProperty('getItem')
|
|
53
|
+
expect(result.current).toHaveProperty('setItem')
|
|
54
|
+
expect(result.current).toHaveProperty('removeItem')
|
|
55
|
+
expect(result.current).toHaveProperty('getAllKeys')
|
|
56
|
+
expect(result.current).toHaveProperty('clear')
|
|
57
|
+
|
|
58
|
+
expect(typeof result.current.getItem).toBe('function')
|
|
59
|
+
expect(typeof result.current.setItem).toBe('function')
|
|
60
|
+
expect(typeof result.current.removeItem).toBe('function')
|
|
61
|
+
expect(typeof result.current.getAllKeys).toBe('function')
|
|
62
|
+
expect(typeof result.current.clear).toBe('function')
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('getItem', () => {
|
|
67
|
+
it('calls getPersistedItem with correct parameters', async () => {
|
|
68
|
+
const {result} = renderHook(() => useAsyncStorage())
|
|
69
|
+
|
|
70
|
+
await act(async () => {
|
|
71
|
+
const value = await result.current.getItem({key: 'test-key'})
|
|
72
|
+
expect(value).toBe('stored-value')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
expect(mockActions.getPersistedItem).toHaveBeenCalledWith({
|
|
76
|
+
key: 'test-key',
|
|
77
|
+
})
|
|
78
|
+
expect(mockActions.getPersistedItem).toHaveBeenCalledTimes(1)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('returns null when item does not exist', async () => {
|
|
82
|
+
mockActions.getPersistedItem.mockResolvedValue(null)
|
|
83
|
+
const {result} = renderHook(() => useAsyncStorage())
|
|
84
|
+
|
|
85
|
+
await act(async () => {
|
|
86
|
+
const value = await result.current.getItem({key: 'non-existent'})
|
|
87
|
+
expect(value).toBeNull()
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('setItem', () => {
|
|
93
|
+
it('calls setPersistedItem with correct parameters', async () => {
|
|
94
|
+
const {result} = renderHook(() => useAsyncStorage())
|
|
95
|
+
|
|
96
|
+
await act(async () => {
|
|
97
|
+
await result.current.setItem({key: 'test-key', value: 'test-value'})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
expect(mockActions.setPersistedItem).toHaveBeenCalledWith({
|
|
101
|
+
key: 'test-key',
|
|
102
|
+
value: 'test-value',
|
|
103
|
+
})
|
|
104
|
+
expect(mockActions.setPersistedItem).toHaveBeenCalledTimes(1)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('handles empty string values', async () => {
|
|
108
|
+
const {result} = renderHook(() => useAsyncStorage())
|
|
109
|
+
|
|
110
|
+
await act(async () => {
|
|
111
|
+
await result.current.setItem({key: 'test-key', value: ''})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
expect(mockActions.setPersistedItem).toHaveBeenCalledWith({
|
|
115
|
+
key: 'test-key',
|
|
116
|
+
value: '',
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('removeItem', () => {
|
|
122
|
+
it('calls removePersistedItem with correct parameters', async () => {
|
|
123
|
+
const {result} = renderHook(() => useAsyncStorage())
|
|
124
|
+
|
|
125
|
+
await act(async () => {
|
|
126
|
+
await result.current.removeItem({key: 'test-key'})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
expect(mockActions.removePersistedItem).toHaveBeenCalledWith({
|
|
130
|
+
key: 'test-key',
|
|
131
|
+
})
|
|
132
|
+
expect(mockActions.removePersistedItem).toHaveBeenCalledTimes(1)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('getAllKeys', () => {
|
|
137
|
+
it('returns all storage keys', async () => {
|
|
138
|
+
const {result} = renderHook(() => useAsyncStorage())
|
|
139
|
+
|
|
140
|
+
await act(async () => {
|
|
141
|
+
const keys = await result.current.getAllKeys()
|
|
142
|
+
expect(keys).toEqual(['key1', 'key2', 'key3'])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
expect(mockActions.getAllPersistedKeys).toHaveBeenCalledWith()
|
|
146
|
+
expect(mockActions.getAllPersistedKeys).toHaveBeenCalledTimes(1)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('returns empty array when no keys exist', async () => {
|
|
150
|
+
mockActions.getAllPersistedKeys.mockResolvedValue([])
|
|
151
|
+
const {result} = renderHook(() => useAsyncStorage())
|
|
152
|
+
|
|
153
|
+
await act(async () => {
|
|
154
|
+
const keys = await result.current.getAllKeys()
|
|
155
|
+
expect(keys).toEqual([])
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('clear', () => {
|
|
161
|
+
it('calls clearPersistedItems', async () => {
|
|
162
|
+
const {result} = renderHook(() => useAsyncStorage())
|
|
163
|
+
|
|
164
|
+
await act(async () => {
|
|
165
|
+
await result.current.clear()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
expect(mockActions.clearPersistedItems).toHaveBeenCalledWith()
|
|
169
|
+
expect(mockActions.clearPersistedItems).toHaveBeenCalledTimes(1)
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe('Error Handling', () => {
|
|
174
|
+
it('propagates errors from getItem', async () => {
|
|
175
|
+
const error = new Error('Storage error')
|
|
176
|
+
mockActions.getPersistedItem.mockRejectedValue(error)
|
|
177
|
+
const {result} = renderHook(() => useAsyncStorage())
|
|
178
|
+
|
|
179
|
+
await act(async () => {
|
|
180
|
+
await expect(result.current.getItem({key: 'test-key'})).rejects.toThrow(
|
|
181
|
+
'Storage error'
|
|
182
|
+
)
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('propagates errors from setItem', async () => {
|
|
187
|
+
const error = new Error('Write error')
|
|
188
|
+
mockActions.setPersistedItem.mockRejectedValue(error)
|
|
189
|
+
const {result} = renderHook(() => useAsyncStorage())
|
|
190
|
+
|
|
191
|
+
await act(async () => {
|
|
192
|
+
await expect(
|
|
193
|
+
result.current.setItem({key: 'test-key', value: 'test-value'})
|
|
194
|
+
).rejects.toThrow('Write error')
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('propagates errors from clear', async () => {
|
|
199
|
+
const error = new Error('Clear error')
|
|
200
|
+
mockActions.clearPersistedItems.mockRejectedValue(error)
|
|
201
|
+
const {result} = renderHook(() => useAsyncStorage())
|
|
202
|
+
|
|
203
|
+
await act(async () => {
|
|
204
|
+
await expect(result.current.clear()).rejects.toThrow('Clear error')
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
describe('Stability', () => {
|
|
210
|
+
it('maintains function reference stability across renders', () => {
|
|
211
|
+
const {result, rerender} = renderHook(() => useAsyncStorage())
|
|
212
|
+
|
|
213
|
+
const firstRender = {...result.current}
|
|
214
|
+
rerender()
|
|
215
|
+
const secondRender = {...result.current}
|
|
216
|
+
|
|
217
|
+
// Functions should maintain reference equality
|
|
218
|
+
expect(firstRender.getItem).toBe(secondRender.getItem)
|
|
219
|
+
expect(firstRender.setItem).toBe(secondRender.setItem)
|
|
220
|
+
expect(firstRender.removeItem).toBe(secondRender.removeItem)
|
|
221
|
+
expect(firstRender.getAllKeys).toBe(secondRender.getAllKeys)
|
|
222
|
+
expect(firstRender.clear).toBe(secondRender.clear)
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
})
|