@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,182 @@
1
+ import React from 'react';
2
+ import { renderHook } from '@testing-library/react-native';
3
+ import { useDropCompensation } from '../../hooks/drag/useDropCompensation';
4
+ import { DragStateProvider } from '../../contexts/DragStateContext';
5
+ import { createMockSharedValue } from '../utils/test-utils';
6
+
7
+ describe('useDropCompensation', () => {
8
+ const createWrapper = () => {
9
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
10
+ <DragStateProvider>{children}</DragStateProvider>
11
+ );
12
+ return Wrapper;
13
+ };
14
+
15
+ it('does not throw when initialized', () => {
16
+ const translateY = createMockSharedValue(0);
17
+ const shiftY = createMockSharedValue(0);
18
+
19
+ expect(() => {
20
+ renderHook(
21
+ () =>
22
+ useDropCompensation({
23
+ itemId: 'item-1',
24
+ index: 0,
25
+ translateY: translateY as any,
26
+ shiftY: shiftY as any,
27
+ }),
28
+ { wrapper: createWrapper() },
29
+ );
30
+ }).not.toThrow();
31
+ });
32
+
33
+ it('handles index unchanged scenario', () => {
34
+ const translateY = createMockSharedValue(0);
35
+ const shiftY = createMockSharedValue(0);
36
+
37
+ const { rerender } = renderHook(
38
+ ({ index }) =>
39
+ useDropCompensation({
40
+ itemId: 'item-1',
41
+ index,
42
+ translateY: translateY as any,
43
+ shiftY: shiftY as any,
44
+ }),
45
+ {
46
+ wrapper: createWrapper(),
47
+ initialProps: { index: 0 },
48
+ },
49
+ );
50
+
51
+ // Rerender with same index
52
+ rerender({ index: 0 });
53
+
54
+ // Should not modify translateY when index doesn't change
55
+ expect(translateY.value).toBe(0);
56
+ });
57
+
58
+ it('works with different item IDs', () => {
59
+ const translateY = createMockSharedValue(0);
60
+ const shiftY = createMockSharedValue(0);
61
+
62
+ const { rerender } = renderHook(
63
+ ({ itemId, index }) =>
64
+ useDropCompensation({
65
+ itemId,
66
+ index,
67
+ translateY: translateY as any,
68
+ shiftY: shiftY as any,
69
+ }),
70
+ {
71
+ wrapper: createWrapper(),
72
+ initialProps: { itemId: 'item-1', index: 0 },
73
+ },
74
+ );
75
+
76
+ // Rerender with different itemId (view recycling)
77
+ rerender({ itemId: 'item-2', index: 0 });
78
+
79
+ expect(translateY.value).toBe(0);
80
+ });
81
+
82
+ it('tracks index changes via shared value', () => {
83
+ const translateY = createMockSharedValue(0);
84
+ const shiftY = createMockSharedValue(0);
85
+
86
+ const { rerender } = renderHook(
87
+ ({ index }) =>
88
+ useDropCompensation({
89
+ itemId: 'item-1',
90
+ index,
91
+ translateY: translateY as any,
92
+ shiftY: shiftY as any,
93
+ }),
94
+ {
95
+ wrapper: createWrapper(),
96
+ initialProps: { index: 0 },
97
+ },
98
+ );
99
+
100
+ // Rerender with new index
101
+ rerender({ index: 1 });
102
+
103
+ // Hook should track the index change
104
+ });
105
+
106
+ it('handles multiple items with different indices', () => {
107
+ const items = [0, 1, 2, 3, 4].map((index) => {
108
+ const translateY = createMockSharedValue(0);
109
+ const shiftY = createMockSharedValue(0);
110
+
111
+ return renderHook(
112
+ () =>
113
+ useDropCompensation({
114
+ itemId: `item-${index}`,
115
+ index,
116
+ translateY: translateY as any,
117
+ shiftY: shiftY as any,
118
+ }),
119
+ { wrapper: createWrapper() },
120
+ );
121
+ });
122
+
123
+ // All items should be rendered without errors
124
+ expect(items.length).toBe(5);
125
+ });
126
+
127
+ it('unmounts cleanly', () => {
128
+ const translateY = createMockSharedValue(0);
129
+ const shiftY = createMockSharedValue(0);
130
+
131
+ const { unmount } = renderHook(
132
+ () =>
133
+ useDropCompensation({
134
+ itemId: 'item-1',
135
+ index: 0,
136
+ translateY: translateY as any,
137
+ shiftY: shiftY as any,
138
+ }),
139
+ { wrapper: createWrapper() },
140
+ );
141
+
142
+ expect(() => unmount()).not.toThrow();
143
+ });
144
+
145
+ it('works with non-zero initial shiftY', () => {
146
+ const translateY = createMockSharedValue(0);
147
+ const shiftY = createMockSharedValue(80);
148
+
149
+ const { result } = renderHook(
150
+ () =>
151
+ useDropCompensation({
152
+ itemId: 'item-1',
153
+ index: 0,
154
+ translateY: translateY as any,
155
+ shiftY: shiftY as any,
156
+ }),
157
+ { wrapper: createWrapper() },
158
+ );
159
+
160
+ // Hook should handle non-zero shiftY
161
+ expect(shiftY.value).toBe(80);
162
+ });
163
+
164
+ it('works with non-zero initial translateY', () => {
165
+ const translateY = createMockSharedValue(100);
166
+ const shiftY = createMockSharedValue(0);
167
+
168
+ renderHook(
169
+ () =>
170
+ useDropCompensation({
171
+ itemId: 'item-1',
172
+ index: 0,
173
+ translateY: translateY as any,
174
+ shiftY: shiftY as any,
175
+ }),
176
+ { wrapper: createWrapper() },
177
+ );
178
+
179
+ // Hook should handle non-zero translateY
180
+ expect(translateY.value).toBe(100);
181
+ });
182
+ });
@@ -0,0 +1,135 @@
1
+ import React from 'react';
2
+ import { renderHook } from '@testing-library/react-native';
3
+ import { useListEntryAnimation } from '../../hooks/animations/useListEntryAnimation';
4
+ import { ListAnimationProvider } from '../../contexts/ListAnimationContext';
5
+
6
+ describe('useListEntryAnimation', () => {
7
+ const createWrapper = () => {
8
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
9
+ <ListAnimationProvider>{children}</ListAnimationProvider>
10
+ );
11
+ return Wrapper;
12
+ };
13
+
14
+ it('returns entryAnimatedStyle', () => {
15
+ const { result } = renderHook(() => useListEntryAnimation('item-1'), {
16
+ wrapper: createWrapper(),
17
+ });
18
+
19
+ expect(result.current.entryAnimatedStyle).toBeDefined();
20
+ });
21
+
22
+ it('works without ListAnimationProvider (optional context)', () => {
23
+ // Should not throw when used without provider
24
+ const { result } = renderHook(() => useListEntryAnimation('item-1'));
25
+
26
+ expect(result.current.entryAnimatedStyle).toBeDefined();
27
+ });
28
+
29
+ it('accepts custom config overrides', () => {
30
+ const { result } = renderHook(
31
+ () =>
32
+ useListEntryAnimation('item-1', {
33
+ fade: { duration: 500 },
34
+ slide: { distance: 100, duration: 400 },
35
+ }),
36
+ { wrapper: createWrapper() },
37
+ );
38
+
39
+ expect(result.current.entryAnimatedStyle).toBeDefined();
40
+ });
41
+
42
+ it('resets animation state when itemId changes (view recycling)', () => {
43
+ const { result, rerender } = renderHook(
44
+ ({ itemId }) => useListEntryAnimation(itemId),
45
+ {
46
+ wrapper: createWrapper(),
47
+ initialProps: { itemId: 'item-1' },
48
+ },
49
+ );
50
+
51
+ const initialStyle = result.current.entryAnimatedStyle;
52
+ expect(initialStyle).toBeDefined();
53
+
54
+ // Rerender with different itemId (simulates view recycling)
55
+ rerender({ itemId: 'item-2' });
56
+
57
+ // Style should still be defined after recycling
58
+ expect(result.current.entryAnimatedStyle).toBeDefined();
59
+ });
60
+
61
+ it('only checks for entry animation once per item', () => {
62
+ const { result, rerender } = renderHook(
63
+ ({ itemId }) => useListEntryAnimation(itemId),
64
+ {
65
+ wrapper: createWrapper(),
66
+ initialProps: { itemId: 'item-1' },
67
+ },
68
+ );
69
+
70
+ // First render
71
+ expect(result.current.entryAnimatedStyle).toBeDefined();
72
+
73
+ // Rerender same item - should not re-check
74
+ rerender({ itemId: 'item-1' });
75
+ expect(result.current.entryAnimatedStyle).toBeDefined();
76
+ });
77
+
78
+ it('resets check flag when item changes', () => {
79
+ const { result, rerender } = renderHook(
80
+ ({ itemId }) => useListEntryAnimation(itemId),
81
+ {
82
+ wrapper: createWrapper(),
83
+ initialProps: { itemId: 'item-1' },
84
+ },
85
+ );
86
+
87
+ expect(result.current.entryAnimatedStyle).toBeDefined();
88
+
89
+ // Change to different item - should reset check flag
90
+ rerender({ itemId: 'item-2' });
91
+ expect(result.current.entryAnimatedStyle).toBeDefined();
92
+
93
+ // Change back - should check again
94
+ rerender({ itemId: 'item-1' });
95
+ expect(result.current.entryAnimatedStyle).toBeDefined();
96
+ });
97
+
98
+ it('uses default animation config values', () => {
99
+ const { result } = renderHook(() => useListEntryAnimation('item-1'), {
100
+ wrapper: createWrapper(),
101
+ });
102
+
103
+ // Should use defaults without throwing
104
+ expect(result.current.entryAnimatedStyle).toBeDefined();
105
+ });
106
+
107
+ it('partial config overrides merge with defaults', () => {
108
+ const { result } = renderHook(
109
+ () =>
110
+ useListEntryAnimation('item-1', {
111
+ fade: { duration: 500 },
112
+ // slide not specified - should use defaults
113
+ }),
114
+ { wrapper: createWrapper() },
115
+ );
116
+
117
+ expect(result.current.entryAnimatedStyle).toBeDefined();
118
+ });
119
+
120
+ it('handles multiple items simultaneously', () => {
121
+ const item1 = renderHook(() => useListEntryAnimation('item-1'), {
122
+ wrapper: createWrapper(),
123
+ });
124
+ const item2 = renderHook(() => useListEntryAnimation('item-2'), {
125
+ wrapper: createWrapper(),
126
+ });
127
+ const item3 = renderHook(() => useListEntryAnimation('item-3'), {
128
+ wrapper: createWrapper(),
129
+ });
130
+
131
+ expect(item1.result.current.entryAnimatedStyle).toBeDefined();
132
+ expect(item2.result.current.entryAnimatedStyle).toBeDefined();
133
+ expect(item3.result.current.entryAnimatedStyle).toBeDefined();
134
+ });
135
+ });
@@ -0,0 +1,175 @@
1
+ import { renderHook, act } from '@testing-library/react-native';
2
+ import { useListExitAnimation } from '../../hooks/animations/useListExitAnimation';
3
+
4
+ // Mock scheduleOnRN to execute synchronously in tests
5
+ jest.mock('react-native-worklets', () => ({
6
+ scheduleOnRN: jest.fn((fn) => fn()),
7
+ }));
8
+
9
+ describe('useListExitAnimation', () => {
10
+ beforeEach(() => {
11
+ jest.clearAllMocks();
12
+ });
13
+
14
+ it('returns exitAnimatedStyle, triggerExit, and resetAnimation', () => {
15
+ const { result } = renderHook(() => useListExitAnimation('item-1'));
16
+
17
+ expect(result.current.exitAnimatedStyle).toBeDefined();
18
+ expect(typeof result.current.triggerExit).toBe('function');
19
+ expect(typeof result.current.resetAnimation).toBe('function');
20
+ });
21
+
22
+ it('exitAnimatedStyle returns static values when no animation is active', () => {
23
+ const { result } = renderHook(() => useListExitAnimation('item-1'));
24
+
25
+ // The animated style should return static values
26
+ const style = result.current.exitAnimatedStyle;
27
+ expect(style).toBeDefined();
28
+ });
29
+
30
+ it('triggerExit accepts direction and onComplete callback', () => {
31
+ const { result } = renderHook(() => useListExitAnimation('item-1'));
32
+
33
+ const onComplete = jest.fn();
34
+
35
+ act(() => {
36
+ result.current.triggerExit(1, onComplete);
37
+ });
38
+
39
+ // Function should be callable without throwing
40
+ expect(onComplete).not.toThrow;
41
+ });
42
+
43
+ it('triggerExit with direction -1 (backward)', () => {
44
+ const { result } = renderHook(() => useListExitAnimation('item-1'));
45
+
46
+ const onComplete = jest.fn();
47
+
48
+ act(() => {
49
+ result.current.triggerExit(-1, onComplete);
50
+ });
51
+
52
+ // Function should be callable without throwing
53
+ expect(onComplete).not.toThrow;
54
+ });
55
+
56
+ it('triggerExit guards against rapid toggling', () => {
57
+ const { result } = renderHook(() => useListExitAnimation('item-1'));
58
+
59
+ const onComplete1 = jest.fn();
60
+ const onComplete2 = jest.fn();
61
+
62
+ act(() => {
63
+ result.current.triggerExit(1, onComplete1);
64
+ // This should be ignored due to isAnimating guard
65
+ result.current.triggerExit(-1, onComplete2);
66
+ });
67
+
68
+ // Only first animation should be triggered
69
+ // The guard prevents rapid toggling
70
+ });
71
+
72
+ it('resetAnimation clears animation state', () => {
73
+ const { result } = renderHook(() => useListExitAnimation('item-1'));
74
+
75
+ act(() => {
76
+ result.current.resetAnimation();
77
+ });
78
+
79
+ // After reset, animation should be in default state
80
+ expect(result.current.exitAnimatedStyle).toBeDefined();
81
+ });
82
+
83
+ it('resets state when itemId changes (view recycling)', () => {
84
+ const { result, rerender } = renderHook(
85
+ ({ itemId }) => useListExitAnimation(itemId),
86
+ { initialProps: { itemId: 'item-1' } },
87
+ );
88
+
89
+ const onComplete = jest.fn();
90
+ act(() => {
91
+ result.current.triggerExit(1, onComplete);
92
+ });
93
+
94
+ // Rerender with different itemId (simulates view recycling)
95
+ rerender({ itemId: 'item-2' });
96
+
97
+ // Animation state should be reset for the new item
98
+ expect(result.current.exitAnimatedStyle).toBeDefined();
99
+ });
100
+
101
+ it('uses fast preset by default', () => {
102
+ const { result } = renderHook(() => useListExitAnimation('item-1'));
103
+
104
+ const onComplete = jest.fn();
105
+
106
+ act(() => {
107
+ result.current.triggerExit(1, onComplete);
108
+ });
109
+
110
+ // Fast preset should be used by default
111
+ expect(result.current.triggerExit).toBeDefined();
112
+ });
113
+
114
+ it('supports default preset', () => {
115
+ const { result } = renderHook(() => useListExitAnimation('item-1'));
116
+
117
+ const onComplete = jest.fn();
118
+
119
+ act(() => {
120
+ result.current.triggerExit(1, onComplete, 'default');
121
+ });
122
+
123
+ // Default preset should be usable
124
+ expect(result.current.triggerExit).toBeDefined();
125
+ });
126
+
127
+ it('calls onExitStart callback when configured', () => {
128
+ const onExitStart = jest.fn();
129
+ const onExitComplete = jest.fn();
130
+
131
+ const { result } = renderHook(() =>
132
+ useListExitAnimation('item-1', {
133
+ index: 2,
134
+ measuredHeight: 80,
135
+ onExitStart,
136
+ onExitComplete,
137
+ }),
138
+ );
139
+
140
+ const onComplete = jest.fn();
141
+
142
+ act(() => {
143
+ result.current.triggerExit(1, onComplete);
144
+ });
145
+
146
+ expect(onExitStart).toHaveBeenCalledWith(2, 80);
147
+ });
148
+
149
+ it('calls onExitComplete when animation finishes', () => {
150
+ const onExitComplete = jest.fn();
151
+
152
+ const { result } = renderHook(() =>
153
+ useListExitAnimation('item-1', {
154
+ onExitComplete,
155
+ }),
156
+ );
157
+
158
+ const onComplete = jest.fn();
159
+
160
+ act(() => {
161
+ result.current.triggerExit(1, onComplete);
162
+ });
163
+
164
+ // Due to mocking, callback may be called synchronously
165
+ // In real usage, it would be called after animation completes
166
+ });
167
+
168
+ it('unmounts cleanly without errors', () => {
169
+ const { unmount } = renderHook(() => useListExitAnimation('item-1'));
170
+
171
+ expect(() => {
172
+ unmount();
173
+ }).not.toThrow();
174
+ });
175
+ });
@@ -0,0 +1,159 @@
1
+ import React, { ReactElement } from 'react';
2
+ import { render, RenderOptions } from '@testing-library/react-native';
3
+ import { DragStateProvider } from '../../contexts/DragStateContext';
4
+ import { ListAnimationProvider } from '../../contexts/ListAnimationContext';
5
+ import type { DragConfig } from '../../types';
6
+
7
+ /**
8
+ * Props for the AllProviders wrapper
9
+ */
10
+ interface AllProvidersProps {
11
+ children: React.ReactNode;
12
+ dragConfig?: Partial<DragConfig>;
13
+ entryAnimationTimeout?: number;
14
+ layoutAnimationDuration?: number;
15
+ }
16
+
17
+ /**
18
+ * Wrapper component that provides all required contexts for testing
19
+ */
20
+ const AllProviders: React.FC<AllProvidersProps> = ({
21
+ children,
22
+ dragConfig,
23
+ entryAnimationTimeout,
24
+ layoutAnimationDuration,
25
+ }) => {
26
+ return (
27
+ <ListAnimationProvider
28
+ entryAnimationTimeout={entryAnimationTimeout}
29
+ layoutAnimationDuration={layoutAnimationDuration}
30
+ >
31
+ <DragStateProvider config={dragConfig}>{children}</DragStateProvider>
32
+ </ListAnimationProvider>
33
+ );
34
+ };
35
+
36
+ /**
37
+ * Custom render options with context configuration
38
+ */
39
+ interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
40
+ dragConfig?: Partial<DragConfig>;
41
+ entryAnimationTimeout?: number;
42
+ layoutAnimationDuration?: number;
43
+ }
44
+
45
+ /**
46
+ * Custom render function that wraps components with all necessary providers
47
+ */
48
+ const customRender = (
49
+ ui: ReactElement,
50
+ options?: CustomRenderOptions,
51
+ ): ReturnType<typeof render> => {
52
+ const {
53
+ dragConfig,
54
+ entryAnimationTimeout,
55
+ layoutAnimationDuration,
56
+ ...renderOptions
57
+ } = options ?? {};
58
+
59
+ const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
60
+ <AllProviders
61
+ dragConfig={dragConfig}
62
+ entryAnimationTimeout={entryAnimationTimeout}
63
+ layoutAnimationDuration={layoutAnimationDuration}
64
+ >
65
+ {children}
66
+ </AllProviders>
67
+ );
68
+
69
+ return render(ui, { wrapper: Wrapper, ...renderOptions });
70
+ };
71
+
72
+ // Re-export everything
73
+ export * from '@testing-library/react-native';
74
+
75
+ // Override render with our custom version
76
+ export { customRender as render };
77
+
78
+ /**
79
+ * Create a mock list item with required id field
80
+ */
81
+ export interface MockListItem {
82
+ id: string;
83
+ title?: string;
84
+ [key: string]: unknown;
85
+ }
86
+
87
+ /**
88
+ * Factory function to create a single mock item
89
+ */
90
+ export const createMockItem = (
91
+ id: string,
92
+ overrides: Partial<MockListItem> = {},
93
+ ): MockListItem => ({
94
+ id,
95
+ title: `Item ${id}`,
96
+ ...overrides,
97
+ });
98
+
99
+ /**
100
+ * Factory function to create multiple mock items
101
+ */
102
+ export const createMockItems = (count: number): MockListItem[] =>
103
+ Array.from({ length: count }, (_, i) => createMockItem(`item-${i + 1}`));
104
+
105
+ /**
106
+ * Create a mock SharedValue-like object for testing
107
+ */
108
+ export const createMockSharedValue = <T,>(initialValue: T) => ({
109
+ value: initialValue,
110
+ addListener: jest.fn(),
111
+ removeListener: jest.fn(),
112
+ modify: jest.fn((modifier: (val: T) => T) => modifier(initialValue)),
113
+ });
114
+
115
+ /**
116
+ * Create a mock animated ref for testing
117
+ */
118
+ export const createMockAnimatedRef = <T,>(current: T | null = null) => ({
119
+ current,
120
+ });
121
+
122
+ /**
123
+ * Wait for animations to complete (mock version)
124
+ */
125
+ export const waitForAnimations = async (ms = 300): Promise<void> => {
126
+ await new Promise((resolve) => setTimeout(resolve, ms));
127
+ };
128
+
129
+ /**
130
+ * Mock gesture event data for testing drag operations
131
+ */
132
+ export const createMockGestureEvent = (overrides: Record<string, unknown> = {}) => ({
133
+ translationX: 0,
134
+ translationY: 0,
135
+ velocityX: 0,
136
+ velocityY: 0,
137
+ absoluteX: 0,
138
+ absoluteY: 0,
139
+ x: 0,
140
+ y: 0,
141
+ numberOfPointers: 1,
142
+ state: 4, // ACTIVE
143
+ ...overrides,
144
+ });
145
+
146
+ /**
147
+ * Mock FlashList ref for testing
148
+ */
149
+ export const createMockFlashListRef = () => ({
150
+ scrollToOffset: jest.fn(),
151
+ scrollToIndex: jest.fn(),
152
+ scrollToItem: jest.fn(),
153
+ scrollToEnd: jest.fn(),
154
+ getScrollOffset: jest.fn(() => 0),
155
+ getScrollableNode: jest.fn(() => null),
156
+ recordInteraction: jest.fn(),
157
+ flashScrollIndicators: jest.fn(),
158
+ prepareForLayoutAnimationRender: jest.fn(),
159
+ });