@souscheflabs/reanimated-flashlist 0.1.7

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 (104) hide show
  1. package/README.md +282 -0
  2. package/lib/AnimatedFlashList.d.ts +6 -0
  3. package/lib/AnimatedFlashList.d.ts.map +1 -0
  4. package/lib/AnimatedFlashList.js +207 -0
  5. package/lib/AnimatedFlashListItem.d.ts +33 -0
  6. package/lib/AnimatedFlashListItem.d.ts.map +1 -0
  7. package/lib/AnimatedFlashListItem.js +155 -0
  8. package/lib/__tests__/utils/test-utils.d.ts +82 -0
  9. package/lib/__tests__/utils/test-utils.d.ts.map +1 -0
  10. package/lib/__tests__/utils/test-utils.js +115 -0
  11. package/lib/constants/animations.d.ts +39 -0
  12. package/lib/constants/animations.d.ts.map +1 -0
  13. package/lib/constants/animations.js +100 -0
  14. package/lib/constants/drag.d.ts +11 -0
  15. package/lib/constants/drag.d.ts.map +1 -0
  16. package/lib/constants/drag.js +47 -0
  17. package/lib/constants/index.d.ts +3 -0
  18. package/lib/constants/index.d.ts.map +1 -0
  19. package/lib/constants/index.js +18 -0
  20. package/lib/contexts/DragStateContext.d.ts +73 -0
  21. package/lib/contexts/DragStateContext.d.ts.map +1 -0
  22. package/lib/contexts/DragStateContext.js +148 -0
  23. package/lib/contexts/ListAnimationContext.d.ts +104 -0
  24. package/lib/contexts/ListAnimationContext.d.ts.map +1 -0
  25. package/lib/contexts/ListAnimationContext.js +184 -0
  26. package/lib/contexts/index.d.ts +5 -0
  27. package/lib/contexts/index.d.ts.map +1 -0
  28. package/lib/contexts/index.js +10 -0
  29. package/lib/hooks/animations/index.d.ts +9 -0
  30. package/lib/hooks/animations/index.d.ts.map +1 -0
  31. package/lib/hooks/animations/index.js +13 -0
  32. package/lib/hooks/animations/useListEntryAnimation.d.ts +38 -0
  33. package/lib/hooks/animations/useListEntryAnimation.d.ts.map +1 -0
  34. package/lib/hooks/animations/useListEntryAnimation.js +90 -0
  35. package/lib/hooks/animations/useListExitAnimation.d.ts +67 -0
  36. package/lib/hooks/animations/useListExitAnimation.d.ts.map +1 -0
  37. package/lib/hooks/animations/useListExitAnimation.js +146 -0
  38. package/lib/hooks/drag/index.d.ts +20 -0
  39. package/lib/hooks/drag/index.d.ts.map +1 -0
  40. package/lib/hooks/drag/index.js +26 -0
  41. package/lib/hooks/drag/useDragAnimatedStyle.d.ts +33 -0
  42. package/lib/hooks/drag/useDragAnimatedStyle.d.ts.map +1 -0
  43. package/lib/hooks/drag/useDragAnimatedStyle.js +61 -0
  44. package/lib/hooks/drag/useDragGesture.d.ts +30 -0
  45. package/lib/hooks/drag/useDragGesture.d.ts.map +1 -0
  46. package/lib/hooks/drag/useDragGesture.js +189 -0
  47. package/lib/hooks/drag/useDragShift.d.ts +21 -0
  48. package/lib/hooks/drag/useDragShift.d.ts.map +1 -0
  49. package/lib/hooks/drag/useDragShift.js +85 -0
  50. package/lib/hooks/drag/useDropCompensation.d.ts +27 -0
  51. package/lib/hooks/drag/useDropCompensation.d.ts.map +1 -0
  52. package/lib/hooks/drag/useDropCompensation.js +90 -0
  53. package/lib/hooks/index.d.ts +8 -0
  54. package/lib/hooks/index.d.ts.map +1 -0
  55. package/lib/hooks/index.js +18 -0
  56. package/lib/index.d.ts +42 -0
  57. package/lib/index.d.ts.map +1 -0
  58. package/lib/index.js +69 -0
  59. package/lib/types/animations.d.ts +71 -0
  60. package/lib/types/animations.d.ts.map +1 -0
  61. package/lib/types/animations.js +2 -0
  62. package/lib/types/drag.d.ts +94 -0
  63. package/lib/types/drag.d.ts.map +1 -0
  64. package/lib/types/drag.js +2 -0
  65. package/lib/types/index.d.ts +4 -0
  66. package/lib/types/index.d.ts.map +1 -0
  67. package/lib/types/index.js +19 -0
  68. package/lib/types/list.d.ts +136 -0
  69. package/lib/types/list.d.ts.map +1 -0
  70. package/lib/types/list.js +2 -0
  71. package/package.json +73 -0
  72. package/src/AnimatedFlashList.tsx +411 -0
  73. package/src/AnimatedFlashListItem.tsx +212 -0
  74. package/src/__tests__/components/AnimatedFlashList.test.tsx +365 -0
  75. package/src/__tests__/components/AnimatedFlashListItem.test.tsx +371 -0
  76. package/src/__tests__/contexts/DragStateContext.test.tsx +169 -0
  77. package/src/__tests__/contexts/ListAnimationContext.test.tsx +324 -0
  78. package/src/__tests__/hooks/useDragAnimatedStyle.test.tsx +118 -0
  79. package/src/__tests__/hooks/useDragGesture.test.tsx +169 -0
  80. package/src/__tests__/hooks/useDragShift.test.tsx +94 -0
  81. package/src/__tests__/hooks/useDropCompensation.test.tsx +182 -0
  82. package/src/__tests__/hooks/useListEntryAnimation.test.tsx +135 -0
  83. package/src/__tests__/hooks/useListExitAnimation.test.tsx +175 -0
  84. package/src/__tests__/utils/test-utils.tsx +159 -0
  85. package/src/constants/animations.ts +107 -0
  86. package/src/constants/drag.ts +51 -0
  87. package/src/constants/index.ts +2 -0
  88. package/src/contexts/DragStateContext.tsx +197 -0
  89. package/src/contexts/ListAnimationContext.tsx +302 -0
  90. package/src/contexts/index.ts +9 -0
  91. package/src/hooks/animations/index.ts +9 -0
  92. package/src/hooks/animations/useListEntryAnimation.ts +108 -0
  93. package/src/hooks/animations/useListExitAnimation.ts +197 -0
  94. package/src/hooks/drag/index.ts +20 -0
  95. package/src/hooks/drag/useDragAnimatedStyle.ts +80 -0
  96. package/src/hooks/drag/useDragGesture.ts +267 -0
  97. package/src/hooks/drag/useDragShift.ts +119 -0
  98. package/src/hooks/drag/useDropCompensation.ts +120 -0
  99. package/src/hooks/index.ts +16 -0
  100. package/src/index.ts +105 -0
  101. package/src/types/animations.ts +76 -0
  102. package/src/types/drag.ts +101 -0
  103. package/src/types/index.ts +3 -0
  104. package/src/types/list.ts +178 -0
@@ -0,0 +1,324 @@
1
+ import React from 'react';
2
+ import { renderHook, act } from '@testing-library/react-native';
3
+ import { LayoutAnimation } from 'react-native';
4
+ import {
5
+ ListAnimationProvider,
6
+ useListAnimation,
7
+ useListAnimationOptional,
8
+ } from '../../contexts/ListAnimationContext';
9
+
10
+ // Mock LayoutAnimation
11
+ jest.mock('react-native/Libraries/LayoutAnimation/LayoutAnimation');
12
+
13
+ describe('ListAnimationContext', () => {
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ jest.useFakeTimers();
17
+ });
18
+
19
+ afterEach(() => {
20
+ jest.useRealTimers();
21
+ });
22
+
23
+ describe('useListAnimation', () => {
24
+ it('throws when used outside ListAnimationProvider', () => {
25
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
26
+
27
+ expect(() => {
28
+ renderHook(() => useListAnimation());
29
+ }).toThrow('useListAnimation must be used within ListAnimationProvider');
30
+
31
+ consoleSpy.mockRestore();
32
+ });
33
+
34
+ it('provides context value when used within ListAnimationProvider', () => {
35
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
36
+ <ListAnimationProvider>{children}</ListAnimationProvider>
37
+ );
38
+
39
+ const { result } = renderHook(() => useListAnimation(), { wrapper });
40
+
41
+ expect(result.current).toBeDefined();
42
+ expect(typeof result.current.registerAnimationTrigger).toBe('function');
43
+ expect(typeof result.current.triggerExitAnimation).toBe('function');
44
+ });
45
+ });
46
+
47
+ describe('useListAnimationOptional', () => {
48
+ it('returns null when used outside ListAnimationProvider', () => {
49
+ const { result } = renderHook(() => useListAnimationOptional());
50
+ expect(result.current).toBeNull();
51
+ });
52
+
53
+ it('returns context value when used within ListAnimationProvider', () => {
54
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
55
+ <ListAnimationProvider>{children}</ListAnimationProvider>
56
+ );
57
+
58
+ const { result } = renderHook(() => useListAnimationOptional(), { wrapper });
59
+ expect(result.current).not.toBeNull();
60
+ });
61
+ });
62
+
63
+ describe('ListAnimationProvider', () => {
64
+ const createWrapper = (props?: Partial<Parameters<typeof ListAnimationProvider>[0]>) => {
65
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
66
+ <ListAnimationProvider {...props}>{children}</ListAnimationProvider>
67
+ );
68
+ return Wrapper;
69
+ };
70
+
71
+ describe('exit animations', () => {
72
+ it('registerAnimationTrigger and unregisterAnimationTrigger work correctly', () => {
73
+ const { result } = renderHook(() => useListAnimation(), {
74
+ wrapper: createWrapper(),
75
+ });
76
+
77
+ const mockTrigger = jest.fn();
78
+
79
+ act(() => {
80
+ result.current.registerAnimationTrigger('item-1', mockTrigger);
81
+ });
82
+
83
+ // Trigger should be called
84
+ const onComplete = jest.fn();
85
+ act(() => {
86
+ const triggered = result.current.triggerExitAnimation('item-1', 1, onComplete);
87
+ expect(triggered).toBe(true);
88
+ });
89
+
90
+ expect(mockTrigger).toHaveBeenCalledWith(1, onComplete);
91
+
92
+ // Unregister
93
+ act(() => {
94
+ result.current.unregisterAnimationTrigger('item-1');
95
+ });
96
+
97
+ // Should not trigger after unregister
98
+ const triggered = result.current.triggerExitAnimation('item-1', 1, onComplete);
99
+ expect(triggered).toBe(false);
100
+ });
101
+
102
+ it('triggerExitAnimation returns false for non-existent item', () => {
103
+ const { result } = renderHook(() => useListAnimation(), {
104
+ wrapper: createWrapper(),
105
+ });
106
+
107
+ const onComplete = jest.fn();
108
+ const triggered = result.current.triggerExitAnimation('non-existent', 1, onComplete);
109
+
110
+ expect(triggered).toBe(false);
111
+ expect(onComplete).not.toHaveBeenCalled();
112
+ });
113
+
114
+ it('triggerExitAnimation passes correct direction and callback', () => {
115
+ const { result } = renderHook(() => useListAnimation(), {
116
+ wrapper: createWrapper(),
117
+ });
118
+
119
+ const mockTrigger = jest.fn();
120
+ const onComplete = jest.fn();
121
+
122
+ act(() => {
123
+ result.current.registerAnimationTrigger('item-1', mockTrigger);
124
+ });
125
+
126
+ // Test direction 1 (down)
127
+ act(() => {
128
+ result.current.triggerExitAnimation('item-1', 1, onComplete);
129
+ });
130
+ expect(mockTrigger).toHaveBeenCalledWith(1, onComplete);
131
+
132
+ mockTrigger.mockClear();
133
+
134
+ // Test direction -1 (up)
135
+ act(() => {
136
+ result.current.triggerExitAnimation('item-1', -1, onComplete);
137
+ });
138
+ expect(mockTrigger).toHaveBeenCalledWith(-1, onComplete);
139
+ });
140
+ });
141
+
142
+ describe('entry animations', () => {
143
+ it('queueEntryAnimation and claimEntryAnimation work correctly', () => {
144
+ const { result } = renderHook(() => useListAnimation(), {
145
+ wrapper: createWrapper(),
146
+ });
147
+
148
+ act(() => {
149
+ result.current.queueEntryAnimation('item-1', 1);
150
+ });
151
+
152
+ let entry: ReturnType<typeof result.current.claimEntryAnimation>;
153
+ act(() => {
154
+ entry = result.current.claimEntryAnimation('item-1');
155
+ });
156
+
157
+ expect(entry).not.toBeNull();
158
+ expect(entry?.itemId).toBe('item-1');
159
+ expect(entry?.direction).toBe(1);
160
+ });
161
+
162
+ it('claimEntryAnimation returns null for non-queued item', () => {
163
+ const { result } = renderHook(() => useListAnimation(), {
164
+ wrapper: createWrapper(),
165
+ });
166
+
167
+ const entry = result.current.claimEntryAnimation('non-existent');
168
+ expect(entry).toBeNull();
169
+ });
170
+
171
+ it('claimEntryAnimation consumes the pending animation', () => {
172
+ const { result } = renderHook(() => useListAnimation(), {
173
+ wrapper: createWrapper(),
174
+ });
175
+
176
+ act(() => {
177
+ result.current.queueEntryAnimation('item-1', 1);
178
+ });
179
+
180
+ // First claim succeeds
181
+ let entry1: ReturnType<typeof result.current.claimEntryAnimation>;
182
+ act(() => {
183
+ entry1 = result.current.claimEntryAnimation('item-1');
184
+ });
185
+ expect(entry1).not.toBeNull();
186
+
187
+ // Second claim fails (already consumed)
188
+ const entry2 = result.current.claimEntryAnimation('item-1');
189
+ expect(entry2).toBeNull();
190
+ });
191
+
192
+ it('entry animation expires after timeout', () => {
193
+ const { result } = renderHook(() => useListAnimation(), {
194
+ wrapper: createWrapper({ entryAnimationTimeout: 1000 }),
195
+ });
196
+
197
+ act(() => {
198
+ result.current.queueEntryAnimation('item-1', 1);
199
+ });
200
+
201
+ // Advance past timeout
202
+ act(() => {
203
+ jest.advanceTimersByTime(1500);
204
+ });
205
+
206
+ // Should be expired
207
+ const entry = result.current.claimEntryAnimation('item-1');
208
+ expect(entry).toBeNull();
209
+ });
210
+
211
+ it('entry animation is valid before timeout', () => {
212
+ const { result } = renderHook(() => useListAnimation(), {
213
+ wrapper: createWrapper({ entryAnimationTimeout: 1000 }),
214
+ });
215
+
216
+ act(() => {
217
+ result.current.queueEntryAnimation('item-1', 1);
218
+ });
219
+
220
+ // Advance but not past timeout
221
+ act(() => {
222
+ jest.advanceTimersByTime(500);
223
+ });
224
+
225
+ // Should still be valid
226
+ let entry: ReturnType<typeof result.current.claimEntryAnimation>;
227
+ act(() => {
228
+ entry = result.current.claimEntryAnimation('item-1');
229
+ });
230
+ expect(entry).not.toBeNull();
231
+ });
232
+ });
233
+
234
+ describe('exiting items for layout compensation', () => {
235
+ it('registerExitingItem and unregisterExitingItem work correctly', () => {
236
+ const { result } = renderHook(() => useListAnimation(), {
237
+ wrapper: createWrapper(),
238
+ });
239
+
240
+ const initialVersion = result.current.exitingItemsVersion.value;
241
+
242
+ act(() => {
243
+ result.current.registerExitingItem('item-1', 0, 80);
244
+ });
245
+
246
+ // Version should increment
247
+ expect(result.current.exitingItemsVersion.value).toBe(initialVersion + 1);
248
+
249
+ act(() => {
250
+ result.current.unregisterExitingItem('item-1');
251
+ });
252
+
253
+ // Version should increment again
254
+ expect(result.current.exitingItemsVersion.value).toBe(initialVersion + 2);
255
+ });
256
+
257
+ it('getExitingHeightAbove returns correct total', () => {
258
+ const { result } = renderHook(() => useListAnimation(), {
259
+ wrapper: createWrapper(),
260
+ });
261
+
262
+ act(() => {
263
+ result.current.registerExitingItem('item-0', 0, 80);
264
+ result.current.registerExitingItem('item-1', 1, 90);
265
+ result.current.registerExitingItem('item-3', 3, 70);
266
+ });
267
+
268
+ // For index 2, items 0 and 1 are above
269
+ const heightAbove2 = result.current.getExitingHeightAbove(2);
270
+ expect(heightAbove2).toBe(170); // 80 + 90
271
+
272
+ // For index 4, all items are above
273
+ const heightAbove4 = result.current.getExitingHeightAbove(4);
274
+ expect(heightAbove4).toBe(240); // 80 + 90 + 70
275
+
276
+ // For index 0, no items are above
277
+ const heightAbove0 = result.current.getExitingHeightAbove(0);
278
+ expect(heightAbove0).toBe(0);
279
+ });
280
+
281
+ it('registerExitingItem triggers LayoutAnimation when enabled', () => {
282
+ const { result } = renderHook(() => useListAnimation(), {
283
+ wrapper: createWrapper({ enableLayoutAnimation: true }),
284
+ });
285
+
286
+ act(() => {
287
+ result.current.registerExitingItem('item-1', 0, 80);
288
+ });
289
+
290
+ expect(LayoutAnimation.configureNext).toHaveBeenCalled();
291
+ });
292
+
293
+ it('registerExitingItem does not trigger LayoutAnimation when disabled', () => {
294
+ const { result } = renderHook(() => useListAnimation(), {
295
+ wrapper: createWrapper({ enableLayoutAnimation: false }),
296
+ });
297
+
298
+ act(() => {
299
+ result.current.registerExitingItem('item-1', 0, 80);
300
+ });
301
+
302
+ expect(LayoutAnimation.configureNext).not.toHaveBeenCalled();
303
+ });
304
+ });
305
+
306
+ describe('configuration', () => {
307
+ it('uses default layoutAnimationDuration', () => {
308
+ const { result } = renderHook(() => useListAnimation(), {
309
+ wrapper: createWrapper(),
310
+ });
311
+
312
+ expect(result.current.layoutAnimationDuration).toBe(200);
313
+ });
314
+
315
+ it('uses custom layoutAnimationDuration', () => {
316
+ const { result } = renderHook(() => useListAnimation(), {
317
+ wrapper: createWrapper({ layoutAnimationDuration: 300 }),
318
+ });
319
+
320
+ expect(result.current.layoutAnimationDuration).toBe(300);
321
+ });
322
+ });
323
+ });
324
+ });
@@ -0,0 +1,118 @@
1
+ import React from 'react';
2
+ import { renderHook } from '@testing-library/react-native';
3
+ import { useDragAnimatedStyle } from '../../hooks/drag/useDragAnimatedStyle';
4
+ import { DragStateProvider } from '../../contexts/DragStateContext';
5
+ import { createMockSharedValue } from '../utils/test-utils';
6
+
7
+ describe('useDragAnimatedStyle', () => {
8
+ const createWrapper = () => {
9
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
10
+ <DragStateProvider>{children}</DragStateProvider>
11
+ );
12
+ return Wrapper;
13
+ };
14
+
15
+ it('returns dragAnimatedStyle', () => {
16
+ const isDragging = createMockSharedValue(false);
17
+ const translateY = createMockSharedValue(0);
18
+ const shiftY = createMockSharedValue(0);
19
+
20
+ const { result } = renderHook(
21
+ () => useDragAnimatedStyle('item-1', isDragging as any, translateY as any, shiftY as any),
22
+ { wrapper: createWrapper() },
23
+ );
24
+
25
+ expect(result.current.dragAnimatedStyle).toBeDefined();
26
+ });
27
+
28
+ it('uses itemId to identify the dragged item', () => {
29
+ const isDragging = createMockSharedValue(false);
30
+ const translateY = createMockSharedValue(0);
31
+ const shiftY = createMockSharedValue(0);
32
+
33
+ const { result } = renderHook(
34
+ () => useDragAnimatedStyle('test-item', isDragging as any, translateY as any, shiftY as any),
35
+ { wrapper: createWrapper() },
36
+ );
37
+
38
+ expect(result.current.dragAnimatedStyle).toBeDefined();
39
+ });
40
+
41
+ it('works with different isDragging states', () => {
42
+ const isDraggingFalse = createMockSharedValue(false);
43
+ const isDraggingTrue = createMockSharedValue(true);
44
+ const translateY = createMockSharedValue(0);
45
+ const shiftY = createMockSharedValue(0);
46
+
47
+ const { result: resultFalse } = renderHook(
48
+ () => useDragAnimatedStyle('item-1', isDraggingFalse as any, translateY as any, shiftY as any),
49
+ { wrapper: createWrapper() },
50
+ );
51
+
52
+ const { result: resultTrue } = renderHook(
53
+ () => useDragAnimatedStyle('item-1', isDraggingTrue as any, translateY as any, shiftY as any),
54
+ { wrapper: createWrapper() },
55
+ );
56
+
57
+ expect(resultFalse.current.dragAnimatedStyle).toBeDefined();
58
+ expect(resultTrue.current.dragAnimatedStyle).toBeDefined();
59
+ });
60
+
61
+ it('handles translateY values', () => {
62
+ const isDragging = createMockSharedValue(true);
63
+ const translateY = createMockSharedValue(100);
64
+ const shiftY = createMockSharedValue(0);
65
+
66
+ const { result } = renderHook(
67
+ () => useDragAnimatedStyle('item-1', isDragging as any, translateY as any, shiftY as any),
68
+ { wrapper: createWrapper() },
69
+ );
70
+
71
+ expect(result.current.dragAnimatedStyle).toBeDefined();
72
+ });
73
+
74
+ it('handles shiftY values', () => {
75
+ const isDragging = createMockSharedValue(false);
76
+ const translateY = createMockSharedValue(0);
77
+ const shiftY = createMockSharedValue(-80);
78
+
79
+ const { result } = renderHook(
80
+ () => useDragAnimatedStyle('item-1', isDragging as any, translateY as any, shiftY as any),
81
+ { wrapper: createWrapper() },
82
+ );
83
+
84
+ expect(result.current.dragAnimatedStyle).toBeDefined();
85
+ });
86
+
87
+ it('handles negative translateY', () => {
88
+ const isDragging = createMockSharedValue(true);
89
+ const translateY = createMockSharedValue(-50);
90
+ const shiftY = createMockSharedValue(0);
91
+
92
+ const { result } = renderHook(
93
+ () => useDragAnimatedStyle('item-1', isDragging as any, translateY as any, shiftY as any),
94
+ { wrapper: createWrapper() },
95
+ );
96
+
97
+ expect(result.current.dragAnimatedStyle).toBeDefined();
98
+ });
99
+
100
+ it('works with custom drag config from provider', () => {
101
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
102
+ <DragStateProvider config={{ dragScale: 1.1, dragShadowOpacity: 0.3 }}>
103
+ {children}
104
+ </DragStateProvider>
105
+ );
106
+
107
+ const isDragging = createMockSharedValue(true);
108
+ const translateY = createMockSharedValue(50);
109
+ const shiftY = createMockSharedValue(0);
110
+
111
+ const { result } = renderHook(
112
+ () => useDragAnimatedStyle('item-1', isDragging as any, translateY as any, shiftY as any),
113
+ { wrapper: Wrapper },
114
+ );
115
+
116
+ expect(result.current.dragAnimatedStyle).toBeDefined();
117
+ });
118
+ });
@@ -0,0 +1,169 @@
1
+ import React from 'react';
2
+ import { renderHook } from '@testing-library/react-native';
3
+ import { useDragGesture } from '../../hooks/drag/useDragGesture';
4
+ import { DragStateProvider } from '../../contexts/DragStateContext';
5
+ import { createMockAnimatedRef } from '../utils/test-utils';
6
+
7
+ // Mock scheduleOnRN to execute synchronously in tests
8
+ jest.mock('react-native-worklets', () => ({
9
+ scheduleOnRN: jest.fn((fn, ...args) => fn(...args)),
10
+ }));
11
+
12
+ describe('useDragGesture', () => {
13
+ const createWrapper = () => {
14
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
15
+ <DragStateProvider>{children}</DragStateProvider>
16
+ );
17
+ return Wrapper;
18
+ };
19
+
20
+ const defaultConfig = {
21
+ itemId: 'item-1',
22
+ index: 0,
23
+ totalItems: 5,
24
+ enabled: true,
25
+ containerRef: createMockAnimatedRef(),
26
+ };
27
+
28
+ const defaultCallbacks = {
29
+ onReorderByDelta: jest.fn(),
30
+ onHapticFeedback: jest.fn(),
31
+ };
32
+
33
+ beforeEach(() => {
34
+ jest.clearAllMocks();
35
+ });
36
+
37
+ it('returns panGesture, isDragging, and translateY', () => {
38
+ const { result } = renderHook(
39
+ () => useDragGesture(defaultConfig, defaultCallbacks),
40
+ { wrapper: createWrapper() },
41
+ );
42
+
43
+ expect(result.current.panGesture).toBeDefined();
44
+ expect(result.current.isDragging).toBeDefined();
45
+ expect(result.current.translateY).toBeDefined();
46
+ });
47
+
48
+ it('initializes isDragging to false', () => {
49
+ const { result } = renderHook(
50
+ () => useDragGesture(defaultConfig, defaultCallbacks),
51
+ { wrapper: createWrapper() },
52
+ );
53
+
54
+ expect(result.current.isDragging.value).toBe(false);
55
+ });
56
+
57
+ it('initializes translateY to 0', () => {
58
+ const { result } = renderHook(
59
+ () => useDragGesture(defaultConfig, defaultCallbacks),
60
+ { wrapper: createWrapper() },
61
+ );
62
+
63
+ expect(result.current.translateY.value).toBe(0);
64
+ });
65
+
66
+ it('creates a Pan gesture with long press activation', () => {
67
+ const { result } = renderHook(
68
+ () => useDragGesture(defaultConfig, defaultCallbacks),
69
+ { wrapper: createWrapper() },
70
+ );
71
+
72
+ // Gesture should be created by Gesture.Pan()
73
+ expect(result.current.panGesture).toBeDefined();
74
+ });
75
+
76
+ it('respects the enabled config', () => {
77
+ const { result } = renderHook(
78
+ () =>
79
+ useDragGesture(
80
+ { ...defaultConfig, enabled: false },
81
+ defaultCallbacks,
82
+ ),
83
+ { wrapper: createWrapper() },
84
+ );
85
+
86
+ // Gesture is still created but enabled is set via config
87
+ expect(result.current.panGesture).toBeDefined();
88
+ });
89
+
90
+ it('uses the containerRef for measuring', () => {
91
+ const containerRef = createMockAnimatedRef();
92
+
93
+ const { result } = renderHook(
94
+ () =>
95
+ useDragGesture(
96
+ { ...defaultConfig, containerRef },
97
+ defaultCallbacks,
98
+ ),
99
+ { wrapper: createWrapper() },
100
+ );
101
+
102
+ expect(result.current.panGesture).toBeDefined();
103
+ });
104
+
105
+ it('updates gesture when config changes', () => {
106
+ const { result, rerender } = renderHook(
107
+ ({ config }) => useDragGesture(config, defaultCallbacks),
108
+ {
109
+ wrapper: createWrapper(),
110
+ initialProps: { config: defaultConfig },
111
+ },
112
+ );
113
+
114
+ const initialGesture = result.current.panGesture;
115
+
116
+ rerender({
117
+ config: { ...defaultConfig, enabled: false },
118
+ });
119
+
120
+ // Gesture should be recreated when enabled changes
121
+ expect(result.current.panGesture).toBeDefined();
122
+ });
123
+
124
+ it('accepts different item indices', () => {
125
+ const items = [0, 1, 2, 3, 4].map((index) => {
126
+ const { result } = renderHook(
127
+ () =>
128
+ useDragGesture(
129
+ { ...defaultConfig, index, itemId: `item-${index}` },
130
+ defaultCallbacks,
131
+ ),
132
+ { wrapper: createWrapper() },
133
+ );
134
+ return result.current;
135
+ });
136
+
137
+ items.forEach((item) => {
138
+ expect(item.panGesture).toBeDefined();
139
+ expect(item.isDragging.value).toBe(false);
140
+ expect(item.translateY.value).toBe(0);
141
+ });
142
+ });
143
+
144
+ it('handles haptic feedback callback', () => {
145
+ const onHapticFeedback = jest.fn();
146
+
147
+ const { result } = renderHook(
148
+ () => useDragGesture(defaultConfig, { ...defaultCallbacks, onHapticFeedback }),
149
+ { wrapper: createWrapper() },
150
+ );
151
+
152
+ // Callback should be storable and gesture should be created
153
+ expect(result.current.panGesture).toBeDefined();
154
+ });
155
+
156
+ it('handles reorder callback', () => {
157
+ const onReorderByDelta = jest.fn();
158
+
159
+ const { result } = renderHook(
160
+ () =>
161
+ useDragGesture(defaultConfig, { ...defaultCallbacks, onReorderByDelta }),
162
+ { wrapper: createWrapper() },
163
+ );
164
+
165
+ // Callback should be storable and gesture should be created
166
+ expect(result.current.panGesture).toBeDefined();
167
+ });
168
+
169
+ });
@@ -0,0 +1,94 @@
1
+ import { renderHook } from '@testing-library/react-native';
2
+ import React from 'react';
3
+ import { useDragShift } from '../../hooks/drag/useDragShift';
4
+ import { DragStateProvider } from '../../contexts/DragStateContext';
5
+
6
+ describe('useDragShift', () => {
7
+ const createWrapper = () => {
8
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
9
+ <DragStateProvider>{children}</DragStateProvider>
10
+ );
11
+ return Wrapper;
12
+ };
13
+
14
+ it('returns a shiftY SharedValue', () => {
15
+ const { result } = renderHook(
16
+ () => useDragShift({ itemId: 'item-1', index: 0 }),
17
+ { wrapper: createWrapper() },
18
+ );
19
+
20
+ expect(result.current.shiftY).toBeDefined();
21
+ expect(result.current.shiftY.value).toBe(0);
22
+ });
23
+
24
+ it('initializes shiftY to 0', () => {
25
+ const { result } = renderHook(
26
+ () => useDragShift({ itemId: 'item-1', index: 0 }),
27
+ { wrapper: createWrapper() },
28
+ );
29
+
30
+ expect(result.current.shiftY.value).toBe(0);
31
+ });
32
+
33
+ it('returns 0 shift when not dragging', () => {
34
+ const { result } = renderHook(
35
+ () => useDragShift({ itemId: 'item-1', index: 1 }),
36
+ { wrapper: createWrapper() },
37
+ );
38
+
39
+ // When nothing is being dragged, shiftY should remain 0
40
+ expect(result.current.shiftY.value).toBe(0);
41
+ });
42
+
43
+ it('returns 0 shift for the dragged item itself', () => {
44
+ const { result } = renderHook(
45
+ () => useDragShift({ itemId: 'dragged-item', index: 2 }),
46
+ { wrapper: createWrapper() },
47
+ );
48
+
49
+ // Even if this item is being dragged, it shouldn't shift itself
50
+ expect(result.current.shiftY.value).toBe(0);
51
+ });
52
+
53
+ it('uses item index and id from config', () => {
54
+ const { result } = renderHook(
55
+ () => useDragShift({ itemId: 'test-item', index: 5 }),
56
+ { wrapper: createWrapper() },
57
+ );
58
+
59
+ // Hook should use the provided config values
60
+ expect(result.current.shiftY).toBeDefined();
61
+ });
62
+
63
+ it('handles different item indices', () => {
64
+ const items = [0, 1, 2, 3, 4].map((index) => {
65
+ const { result } = renderHook(
66
+ () => useDragShift({ itemId: `item-${index}`, index }),
67
+ { wrapper: createWrapper() },
68
+ );
69
+ return result.current;
70
+ });
71
+
72
+ // All items should have shiftY defined
73
+ items.forEach((item) => {
74
+ expect(item.shiftY).toBeDefined();
75
+ expect(item.shiftY.value).toBe(0);
76
+ });
77
+ });
78
+
79
+ it('re-renders correctly when config changes', () => {
80
+ const { result, rerender } = renderHook(
81
+ ({ itemId, index }) => useDragShift({ itemId, index }),
82
+ {
83
+ wrapper: createWrapper(),
84
+ initialProps: { itemId: 'item-1', index: 0 },
85
+ },
86
+ );
87
+
88
+ expect(result.current.shiftY.value).toBe(0);
89
+
90
+ rerender({ itemId: 'item-2', index: 1 });
91
+
92
+ expect(result.current.shiftY).toBeDefined();
93
+ });
94
+ });