@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.
- package/README.md +282 -0
- package/lib/AnimatedFlashList.d.ts +6 -0
- package/lib/AnimatedFlashList.d.ts.map +1 -0
- package/lib/AnimatedFlashList.js +207 -0
- package/lib/AnimatedFlashListItem.d.ts +33 -0
- package/lib/AnimatedFlashListItem.d.ts.map +1 -0
- package/lib/AnimatedFlashListItem.js +155 -0
- package/lib/__tests__/utils/test-utils.d.ts +82 -0
- package/lib/__tests__/utils/test-utils.d.ts.map +1 -0
- package/lib/__tests__/utils/test-utils.js +115 -0
- package/lib/constants/animations.d.ts +39 -0
- package/lib/constants/animations.d.ts.map +1 -0
- package/lib/constants/animations.js +100 -0
- package/lib/constants/drag.d.ts +11 -0
- package/lib/constants/drag.d.ts.map +1 -0
- package/lib/constants/drag.js +47 -0
- package/lib/constants/index.d.ts +3 -0
- package/lib/constants/index.d.ts.map +1 -0
- package/lib/constants/index.js +18 -0
- package/lib/contexts/DragStateContext.d.ts +73 -0
- package/lib/contexts/DragStateContext.d.ts.map +1 -0
- package/lib/contexts/DragStateContext.js +148 -0
- package/lib/contexts/ListAnimationContext.d.ts +104 -0
- package/lib/contexts/ListAnimationContext.d.ts.map +1 -0
- package/lib/contexts/ListAnimationContext.js +184 -0
- package/lib/contexts/index.d.ts +5 -0
- package/lib/contexts/index.d.ts.map +1 -0
- package/lib/contexts/index.js +10 -0
- package/lib/hooks/animations/index.d.ts +9 -0
- package/lib/hooks/animations/index.d.ts.map +1 -0
- package/lib/hooks/animations/index.js +13 -0
- package/lib/hooks/animations/useListEntryAnimation.d.ts +38 -0
- package/lib/hooks/animations/useListEntryAnimation.d.ts.map +1 -0
- package/lib/hooks/animations/useListEntryAnimation.js +90 -0
- package/lib/hooks/animations/useListExitAnimation.d.ts +67 -0
- package/lib/hooks/animations/useListExitAnimation.d.ts.map +1 -0
- package/lib/hooks/animations/useListExitAnimation.js +146 -0
- package/lib/hooks/drag/index.d.ts +20 -0
- package/lib/hooks/drag/index.d.ts.map +1 -0
- package/lib/hooks/drag/index.js +26 -0
- package/lib/hooks/drag/useDragAnimatedStyle.d.ts +33 -0
- package/lib/hooks/drag/useDragAnimatedStyle.d.ts.map +1 -0
- package/lib/hooks/drag/useDragAnimatedStyle.js +61 -0
- package/lib/hooks/drag/useDragGesture.d.ts +30 -0
- package/lib/hooks/drag/useDragGesture.d.ts.map +1 -0
- package/lib/hooks/drag/useDragGesture.js +189 -0
- package/lib/hooks/drag/useDragShift.d.ts +21 -0
- package/lib/hooks/drag/useDragShift.d.ts.map +1 -0
- package/lib/hooks/drag/useDragShift.js +85 -0
- package/lib/hooks/drag/useDropCompensation.d.ts +27 -0
- package/lib/hooks/drag/useDropCompensation.d.ts.map +1 -0
- package/lib/hooks/drag/useDropCompensation.js +90 -0
- package/lib/hooks/index.d.ts +8 -0
- package/lib/hooks/index.d.ts.map +1 -0
- package/lib/hooks/index.js +18 -0
- package/lib/index.d.ts +42 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +69 -0
- package/lib/types/animations.d.ts +71 -0
- package/lib/types/animations.d.ts.map +1 -0
- package/lib/types/animations.js +2 -0
- package/lib/types/drag.d.ts +94 -0
- package/lib/types/drag.d.ts.map +1 -0
- package/lib/types/drag.js +2 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +19 -0
- package/lib/types/list.d.ts +136 -0
- package/lib/types/list.d.ts.map +1 -0
- package/lib/types/list.js +2 -0
- package/package.json +73 -0
- package/src/AnimatedFlashList.tsx +411 -0
- package/src/AnimatedFlashListItem.tsx +212 -0
- package/src/__tests__/components/AnimatedFlashList.test.tsx +365 -0
- package/src/__tests__/components/AnimatedFlashListItem.test.tsx +371 -0
- package/src/__tests__/contexts/DragStateContext.test.tsx +169 -0
- package/src/__tests__/contexts/ListAnimationContext.test.tsx +324 -0
- package/src/__tests__/hooks/useDragAnimatedStyle.test.tsx +118 -0
- package/src/__tests__/hooks/useDragGesture.test.tsx +169 -0
- package/src/__tests__/hooks/useDragShift.test.tsx +94 -0
- package/src/__tests__/hooks/useDropCompensation.test.tsx +182 -0
- package/src/__tests__/hooks/useListEntryAnimation.test.tsx +135 -0
- package/src/__tests__/hooks/useListExitAnimation.test.tsx +175 -0
- package/src/__tests__/utils/test-utils.tsx +159 -0
- package/src/constants/animations.ts +107 -0
- package/src/constants/drag.ts +51 -0
- package/src/constants/index.ts +2 -0
- package/src/contexts/DragStateContext.tsx +197 -0
- package/src/contexts/ListAnimationContext.tsx +302 -0
- package/src/contexts/index.ts +9 -0
- package/src/hooks/animations/index.ts +9 -0
- package/src/hooks/animations/useListEntryAnimation.ts +108 -0
- package/src/hooks/animations/useListExitAnimation.ts +197 -0
- package/src/hooks/drag/index.ts +20 -0
- package/src/hooks/drag/useDragAnimatedStyle.ts +80 -0
- package/src/hooks/drag/useDragGesture.ts +267 -0
- package/src/hooks/drag/useDragShift.ts +119 -0
- package/src/hooks/drag/useDropCompensation.ts +120 -0
- package/src/hooks/index.ts +16 -0
- package/src/index.ts +105 -0
- package/src/types/animations.ts +76 -0
- package/src/types/drag.ts +101 -0
- package/src/types/index.ts +3 -0
- 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
|
+
});
|