@shopify/flash-list 1.0.1

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 (206) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/LICENSE.md +7 -0
  3. package/README.md +65 -0
  4. package/RNFlashList.podspec +26 -0
  5. package/android/build.gradle +59 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutShadow.kt +94 -0
  8. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutView.kt +79 -0
  9. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutViewManager.kt +69 -0
  10. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/CellContainer.java +16 -0
  11. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/CellContainerImpl.kt +16 -0
  12. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/CellContainerManager.kt +27 -0
  13. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/FlashListPackage.kt +19 -0
  14. package/android/src/test/java/com/shopify/reactnative/flash_list/AutoLayoutShadowTest.kt +146 -0
  15. package/android/src/test/java/com/shopify/reactnative/flash_list/models/Rect.kt +59 -0
  16. package/android/src/test/java/com/shopify/reactnative/flash_list/models/TestCollection.kt +6 -0
  17. package/android/src/test/java/com/shopify/reactnative/flash_list/models/TestDataModel.kt +8 -0
  18. package/android/src/test/resources/LayoutTestData.json +708 -0
  19. package/dist/AnimatedFlashList.d.ts +6 -0
  20. package/dist/AnimatedFlashList.d.ts.map +1 -0
  21. package/dist/AnimatedFlashList.js +8 -0
  22. package/dist/AnimatedFlashList.js.map +1 -0
  23. package/dist/FlashList.d.ts +121 -0
  24. package/dist/FlashList.d.ts.map +1 -0
  25. package/dist/FlashList.js +502 -0
  26. package/dist/FlashList.js.map +1 -0
  27. package/dist/FlashListProps.d.ts +251 -0
  28. package/dist/FlashListProps.d.ts.map +1 -0
  29. package/dist/FlashListProps.js +3 -0
  30. package/dist/FlashListProps.js.map +1 -0
  31. package/dist/GridLayoutProviderWithProps.d.ts +30 -0
  32. package/dist/GridLayoutProviderWithProps.d.ts.map +1 -0
  33. package/dist/GridLayoutProviderWithProps.js +80 -0
  34. package/dist/GridLayoutProviderWithProps.js.map +1 -0
  35. package/dist/PureComponentWrapper.d.ts +22 -0
  36. package/dist/PureComponentWrapper.d.ts.map +1 -0
  37. package/dist/PureComponentWrapper.js +37 -0
  38. package/dist/PureComponentWrapper.js.map +1 -0
  39. package/dist/__tests__/AverageWindow.test.d.ts +2 -0
  40. package/dist/__tests__/AverageWindow.test.d.ts.map +1 -0
  41. package/dist/__tests__/AverageWindow.test.js +69 -0
  42. package/dist/__tests__/AverageWindow.test.js.map +1 -0
  43. package/dist/__tests__/FlashList.test.d.ts +2 -0
  44. package/dist/__tests__/FlashList.test.d.ts.map +1 -0
  45. package/dist/__tests__/FlashList.test.js +656 -0
  46. package/dist/__tests__/FlashList.test.js.map +1 -0
  47. package/dist/__tests__/GridLayoutProviderWithProps.test.d.ts +2 -0
  48. package/dist/__tests__/GridLayoutProviderWithProps.test.d.ts.map +1 -0
  49. package/dist/__tests__/GridLayoutProviderWithProps.test.js +133 -0
  50. package/dist/__tests__/GridLayoutProviderWithProps.test.js.map +1 -0
  51. package/dist/__tests__/PlatformHelper.web.test.d.ts +2 -0
  52. package/dist/__tests__/PlatformHelper.web.test.d.ts.map +1 -0
  53. package/dist/__tests__/PlatformHelper.web.test.js +25 -0
  54. package/dist/__tests__/PlatformHelper.web.test.js.map +1 -0
  55. package/dist/__tests__/ViewabilityHelper.test.d.ts +2 -0
  56. package/dist/__tests__/ViewabilityHelper.test.d.ts.map +1 -0
  57. package/dist/__tests__/ViewabilityHelper.test.js +187 -0
  58. package/dist/__tests__/ViewabilityHelper.test.js.map +1 -0
  59. package/dist/__tests__/helpers/mountFlashList.d.ts +20 -0
  60. package/dist/__tests__/helpers/mountFlashList.d.ts.map +1 -0
  61. package/dist/__tests__/helpers/mountFlashList.js +44 -0
  62. package/dist/__tests__/helpers/mountFlashList.js.map +1 -0
  63. package/dist/__tests__/useBlankAreaTracker.test.d.ts +2 -0
  64. package/dist/__tests__/useBlankAreaTracker.test.d.ts.map +1 -0
  65. package/dist/__tests__/useBlankAreaTracker.test.js +179 -0
  66. package/dist/__tests__/useBlankAreaTracker.test.js.map +1 -0
  67. package/dist/benchmark/AutoScrollHelper.d.ts +18 -0
  68. package/dist/benchmark/AutoScrollHelper.d.ts.map +1 -0
  69. package/dist/benchmark/AutoScrollHelper.js +68 -0
  70. package/dist/benchmark/AutoScrollHelper.js.map +1 -0
  71. package/dist/benchmark/JSFPSMonitor.d.ts +23 -0
  72. package/dist/benchmark/JSFPSMonitor.d.ts.map +1 -0
  73. package/dist/benchmark/JSFPSMonitor.js +65 -0
  74. package/dist/benchmark/JSFPSMonitor.js.map +1 -0
  75. package/dist/benchmark/roundToDecimalPlaces.d.ts +2 -0
  76. package/dist/benchmark/roundToDecimalPlaces.d.ts.map +1 -0
  77. package/dist/benchmark/roundToDecimalPlaces.js +9 -0
  78. package/dist/benchmark/roundToDecimalPlaces.js.map +1 -0
  79. package/dist/benchmark/useBenchmark.d.ts +35 -0
  80. package/dist/benchmark/useBenchmark.d.ts.map +1 -0
  81. package/dist/benchmark/useBenchmark.js +167 -0
  82. package/dist/benchmark/useBenchmark.js.map +1 -0
  83. package/dist/benchmark/useBlankAreaTracker.d.ts +34 -0
  84. package/dist/benchmark/useBlankAreaTracker.d.ts.map +1 -0
  85. package/dist/benchmark/useBlankAreaTracker.js +67 -0
  86. package/dist/benchmark/useBlankAreaTracker.js.map +1 -0
  87. package/dist/benchmark/useDataMultiplier.d.ts +9 -0
  88. package/dist/benchmark/useDataMultiplier.d.ts.map +1 -0
  89. package/dist/benchmark/useDataMultiplier.js +25 -0
  90. package/dist/benchmark/useDataMultiplier.js.map +1 -0
  91. package/dist/benchmark/useFlatListBenchmark.d.ts +13 -0
  92. package/dist/benchmark/useFlatListBenchmark.d.ts.map +1 -0
  93. package/dist/benchmark/useFlatListBenchmark.js +100 -0
  94. package/dist/benchmark/useFlatListBenchmark.js.map +1 -0
  95. package/dist/errors/CustomError.d.ts +8 -0
  96. package/dist/errors/CustomError.d.ts.map +1 -0
  97. package/dist/errors/CustomError.js +14 -0
  98. package/dist/errors/CustomError.js.map +1 -0
  99. package/dist/errors/ExceptionList.d.ts +20 -0
  100. package/dist/errors/ExceptionList.d.ts.map +1 -0
  101. package/dist/errors/ExceptionList.js +22 -0
  102. package/dist/errors/ExceptionList.js.map +1 -0
  103. package/dist/errors/Warnings.d.ts +10 -0
  104. package/dist/errors/Warnings.d.ts.map +1 -0
  105. package/dist/errors/Warnings.js +15 -0
  106. package/dist/errors/Warnings.js.map +1 -0
  107. package/dist/index.d.ts +13 -0
  108. package/dist/index.d.ts.map +1 -0
  109. package/dist/index.js +28 -0
  110. package/dist/index.js.map +1 -0
  111. package/dist/native/auto-layout/AutoLayoutView.d.ts +21 -0
  112. package/dist/native/auto-layout/AutoLayoutView.d.ts.map +1 -0
  113. package/dist/native/auto-layout/AutoLayoutView.js +48 -0
  114. package/dist/native/auto-layout/AutoLayoutView.js.map +1 -0
  115. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.d.ts +4 -0
  116. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.d.ts.map +1 -0
  117. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.js +6 -0
  118. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.js.map +1 -0
  119. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.web.d.ts +5 -0
  120. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.web.d.ts.map +1 -0
  121. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.web.js +6 -0
  122. package/dist/native/auto-layout/AutoLayoutViewNativeComponent.web.js.map +1 -0
  123. package/dist/native/auto-layout/AutoLayoutViewNativeComponentProps.d.ts +14 -0
  124. package/dist/native/auto-layout/AutoLayoutViewNativeComponentProps.d.ts.map +1 -0
  125. package/dist/native/auto-layout/AutoLayoutViewNativeComponentProps.js +3 -0
  126. package/dist/native/auto-layout/AutoLayoutViewNativeComponentProps.js.map +1 -0
  127. package/dist/native/cell-container/CellContainer.d.ts +6 -0
  128. package/dist/native/cell-container/CellContainer.d.ts.map +1 -0
  129. package/dist/native/cell-container/CellContainer.js +9 -0
  130. package/dist/native/cell-container/CellContainer.js.map +1 -0
  131. package/dist/native/cell-container/CellContainer.web.d.ts +7 -0
  132. package/dist/native/cell-container/CellContainer.web.d.ts.map +1 -0
  133. package/dist/native/cell-container/CellContainer.web.js +13 -0
  134. package/dist/native/cell-container/CellContainer.web.js.map +1 -0
  135. package/dist/tsconfig.tsbuildinfo +1 -0
  136. package/dist/utils/AverageWindow.d.ts +21 -0
  137. package/dist/utils/AverageWindow.d.ts.map +1 -0
  138. package/dist/utils/AverageWindow.js +49 -0
  139. package/dist/utils/AverageWindow.js.map +1 -0
  140. package/dist/utils/PlatformHelper.d.ts +14 -0
  141. package/dist/utils/PlatformHelper.d.ts.map +1 -0
  142. package/dist/utils/PlatformHelper.js +16 -0
  143. package/dist/utils/PlatformHelper.js.map +1 -0
  144. package/dist/utils/PlatformHelper.web.d.ts +14 -0
  145. package/dist/utils/PlatformHelper.web.d.ts.map +1 -0
  146. package/dist/utils/PlatformHelper.web.js +18 -0
  147. package/dist/utils/PlatformHelper.web.js.map +1 -0
  148. package/dist/viewability/ViewToken.d.ts +8 -0
  149. package/dist/viewability/ViewToken.d.ts.map +1 -0
  150. package/dist/viewability/ViewToken.js +3 -0
  151. package/dist/viewability/ViewToken.js.map +1 -0
  152. package/dist/viewability/ViewabilityHelper.d.ts +25 -0
  153. package/dist/viewability/ViewabilityHelper.d.ts.map +1 -0
  154. package/dist/viewability/ViewabilityHelper.js +104 -0
  155. package/dist/viewability/ViewabilityHelper.js.map +1 -0
  156. package/dist/viewability/ViewabilityManager.d.ts +24 -0
  157. package/dist/viewability/ViewabilityManager.d.ts.map +1 -0
  158. package/dist/viewability/ViewabilityManager.js +94 -0
  159. package/dist/viewability/ViewabilityManager.js.map +1 -0
  160. package/ios/RNFlashList.xcodeproj/project.pbxproj +3 -0
  161. package/ios/RNFlashList.xcodeproj/project.xcworkspace/contents.xcworkspacedata +4 -0
  162. package/ios/Sources/AutoLayoutView.swift +218 -0
  163. package/ios/Sources/AutoLayoutViewManager.m +14 -0
  164. package/ios/Sources/AutoLayoutViewManager.swift +12 -0
  165. package/ios/Sources/CellContainer.swift +9 -0
  166. package/ios/Sources/CellContainerManager.m +8 -0
  167. package/ios/Sources/CellContainerManager.swift +12 -0
  168. package/ios/Sources/FlatListPro-Bridging-Header.h +8 -0
  169. package/ios/Tests/AutoLayoutViewTests.swift +113 -0
  170. package/jestSetup.js +15 -0
  171. package/package.json +75 -0
  172. package/src/AnimatedFlashList.ts +11 -0
  173. package/src/FlashList.tsx +801 -0
  174. package/src/FlashListProps.ts +312 -0
  175. package/src/GridLayoutProviderWithProps.ts +137 -0
  176. package/src/PureComponentWrapper.tsx +42 -0
  177. package/src/__tests__/AverageWindow.test.ts +80 -0
  178. package/src/__tests__/FlashList.test.tsx +738 -0
  179. package/src/__tests__/GridLayoutProviderWithProps.test.ts +150 -0
  180. package/src/__tests__/PlatformHelper.web.test.ts +29 -0
  181. package/src/__tests__/ViewabilityHelper.test.ts +283 -0
  182. package/src/__tests__/helpers/mountFlashList.tsx +62 -0
  183. package/src/__tests__/useBlankAreaTracker.test.tsx +206 -0
  184. package/src/benchmark/AutoScrollHelper.ts +70 -0
  185. package/src/benchmark/JSFPSMonitor.ts +74 -0
  186. package/src/benchmark/roundToDecimalPlaces.ts +4 -0
  187. package/src/benchmark/useBenchmark.ts +240 -0
  188. package/src/benchmark/useBlankAreaTracker.ts +117 -0
  189. package/src/benchmark/useDataMultiplier.ts +19 -0
  190. package/src/benchmark/useFlatListBenchmark.ts +107 -0
  191. package/src/errors/CustomError.ts +10 -0
  192. package/src/errors/ExceptionList.ts +23 -0
  193. package/src/errors/Warnings.ts +18 -0
  194. package/src/index.ts +32 -0
  195. package/src/native/auto-layout/AutoLayoutView.tsx +72 -0
  196. package/src/native/auto-layout/AutoLayoutViewNativeComponent.ts +7 -0
  197. package/src/native/auto-layout/AutoLayoutViewNativeComponent.web.ts +8 -0
  198. package/src/native/auto-layout/AutoLayoutViewNativeComponentProps.ts +14 -0
  199. package/src/native/cell-container/CellContainer.ts +7 -0
  200. package/src/native/cell-container/CellContainer.web.tsx +9 -0
  201. package/src/utils/AverageWindow.ts +49 -0
  202. package/src/utils/PlatformHelper.ts +16 -0
  203. package/src/utils/PlatformHelper.web.ts +20 -0
  204. package/src/viewability/ViewToken.ts +7 -0
  205. package/src/viewability/ViewabilityHelper.ts +162 -0
  206. package/src/viewability/ViewabilityManager.ts +133 -0
@@ -0,0 +1,150 @@
1
+ import { ScrollView } from "react-native";
2
+ import { ProgressiveListView } from "recyclerlistview";
3
+
4
+ import FlashList from "../FlashList";
5
+
6
+ import { mountFlashList } from "./helpers/mountFlashList";
7
+
8
+ describe("GridLayoutProviderWithProps", () => {
9
+ it("updates average window on layout manager change", () => {
10
+ const flashList = mountFlashList();
11
+ const oldAverageWindow = (flashList.instance as FlashList<any>).state
12
+ .layoutProvider["averageWindow"];
13
+
14
+ // width change from default 400 to 600 will force layout manager to change
15
+ flashList.find(ScrollView)?.trigger("onLayout", {
16
+ nativeEvent: { layout: { height: 900, width: 600 } },
17
+ });
18
+
19
+ const newAverageWindow =
20
+ flashList.instance.state.layoutProvider["averageWindow"];
21
+
22
+ expect(newAverageWindow).not.toBe(oldAverageWindow);
23
+ flashList.unmount();
24
+ });
25
+ it("average window's size is two times the number of items that will fill the screen", () => {
26
+ const flashList = mountFlashList({ numColumns: 2 });
27
+ expect(
28
+ flashList.instance.state.layoutProvider["averageWindow"]["inputValues"]
29
+ .length
30
+ ).toBe(20);
31
+ flashList.find(ScrollView)?.trigger("onLayout", {
32
+ nativeEvent: { layout: { height: 2000, width: 600 } },
33
+ });
34
+ expect(
35
+ flashList.instance.state.layoutProvider["averageWindow"]["inputValues"]
36
+ .length
37
+ ).toBe(40);
38
+ flashList.setProps({ numColumns: 1 });
39
+ expect(
40
+ flashList.instance.state.layoutProvider["averageWindow"]["inputValues"]
41
+ .length
42
+ ).toBe(20);
43
+ flashList.unmount();
44
+ });
45
+ it("average window should not be less than 6 in size", () => {
46
+ const flashList = mountFlashList();
47
+ flashList.find(ScrollView)?.trigger("onLayout", {
48
+ nativeEvent: { layout: { height: 100, width: 100 } },
49
+ });
50
+ expect(
51
+ flashList.instance.state.layoutProvider["averageWindow"]["inputValues"]
52
+ .length
53
+ ).toBe(6);
54
+ flashList.unmount();
55
+ });
56
+ it("vertical list: average should not update when widths change", () => {
57
+ const flashList = mountFlashList();
58
+ const layoutProvider = flashList.instance.state.layoutProvider;
59
+ const oldAverage = layoutProvider.averageItemSize;
60
+
61
+ layoutProvider.getLayoutManager()!.getLayouts()[0].width = 500;
62
+ flashList.find(ProgressiveListView)?.instance.onItemLayout(0);
63
+
64
+ expect(oldAverage).toBe(layoutProvider.averageItemSize);
65
+ flashList.unmount();
66
+ });
67
+ it("horizontal list: average should not update when heights change", () => {
68
+ const flashList = mountFlashList({ horizontal: true });
69
+ const layoutProvider = flashList.instance.state.layoutProvider;
70
+ const oldAverage = layoutProvider.averageItemSize;
71
+
72
+ layoutProvider.getLayoutManager()!.getLayouts()[0].height = 600;
73
+
74
+ // Can throw a no op set state warning. Should be handled in PLV.
75
+ flashList.find(ProgressiveListView)?.instance.onItemLayout(0);
76
+
77
+ expect(oldAverage).toBe(layoutProvider.averageItemSize);
78
+ flashList.unmount();
79
+ });
80
+ it("computes correct average", () => {
81
+ const flashList = mountFlashList();
82
+ const layoutProvider = flashList.instance.state.layoutProvider;
83
+ expect(layoutProvider.averageItemSize).toBe(200);
84
+
85
+ const layouts = layoutProvider.getLayoutManager()!.getLayouts();
86
+ const progressiveListView = flashList.find(ProgressiveListView);
87
+ layouts[0].height = 100;
88
+ layouts[1].height = 200;
89
+ layouts[2].height = 300;
90
+ layouts[3].height = 400;
91
+ progressiveListView?.instance.onItemLayout(0);
92
+ progressiveListView?.instance.onItemLayout(1);
93
+ progressiveListView?.instance.onItemLayout(2);
94
+ progressiveListView?.instance.onItemLayout(3);
95
+
96
+ // estimatedItemSize is treated as one of the values. That's why 240.
97
+ expect(layoutProvider.averageItemSize).toBe(240);
98
+ flashList.unmount();
99
+ });
100
+ it("updates all cached widths for vertical list and heights for horizontal list when orientation changes", () => {
101
+ const runCacheUpdateTest = (horizontal: boolean) => {
102
+ const flashList = mountFlashList({
103
+ data: new Array(20).fill("1"),
104
+ horizontal,
105
+ });
106
+ const progressiveListView = flashList.find(ProgressiveListView);
107
+ const layoutProvider = flashList.instance.state.layoutProvider;
108
+
109
+ layoutProvider
110
+ .getLayoutManager()!
111
+ .getLayouts()
112
+ .forEach((layout, index) => {
113
+ // marking layouts as if they're actual rendered sizes
114
+ progressiveListView?.instance.onItemLayout(index);
115
+
116
+ // checking size
117
+ if (horizontal) {
118
+ expect(layout.height).toBe(900);
119
+ } else {
120
+ expect(layout.width).toBe(400);
121
+ }
122
+ });
123
+
124
+ // simulates orientation change
125
+ flashList.find(ScrollView)?.trigger("onLayout", {
126
+ nativeEvent: { layout: { height: 400, width: 900 } },
127
+ });
128
+
129
+ layoutProvider
130
+ .getLayoutManager()!
131
+ .getLayouts()
132
+ .forEach((layout) => {
133
+ // making sure all widths or heights are updated
134
+ if (horizontal) {
135
+ expect(layout.height).toBe(400);
136
+ } else {
137
+ expect(layout.width).toBe(900);
138
+ }
139
+
140
+ // making sure extra keys don't make their way to layout unnecessarily
141
+ expect(Object.keys(layout).length).toBe(6);
142
+ });
143
+ flashList.unmount();
144
+ };
145
+ // vertical list
146
+ runCacheUpdateTest(false);
147
+ // horizontal list
148
+ runCacheUpdateTest(true);
149
+ });
150
+ });
@@ -0,0 +1,29 @@
1
+ import {
2
+ getCellContainerPlatformStyles,
3
+ getItemAnimator,
4
+ } from "../utils/PlatformHelper.web";
5
+
6
+ describe("Platform Helper Web", () => {
7
+ it("can compute right transform for web", () => {
8
+ const transformStyle = getCellContainerPlatformStyles(false, {
9
+ x: 20,
10
+ y: 70,
11
+ });
12
+ const transformInvertedStyle = getCellContainerPlatformStyles(true, {
13
+ x: 30,
14
+ y: 30,
15
+ });
16
+ const expectedTransform = "translate(20px,70px)";
17
+ const expectedTransformInverted = "translate(30px,30px) scaleY(-1)";
18
+
19
+ expect(transformStyle?.transform).toBe(expectedTransform);
20
+ expect(transformStyle?.WebkitTransform).toBe(expectedTransform);
21
+ expect(transformInvertedStyle?.transform).toBe(expectedTransformInverted);
22
+ expect(transformInvertedStyle?.WebkitTransform).toBe(
23
+ expectedTransformInverted
24
+ );
25
+ });
26
+ it("can return an animator", () => {
27
+ expect(getItemAnimator()!["animateWillMount"]).toBeDefined();
28
+ });
29
+ });
@@ -0,0 +1,283 @@
1
+ import { Dimension, Layout } from "recyclerlistview";
2
+
3
+ import CustomError from "../errors/CustomError";
4
+ import ExceptionList from "../errors/ExceptionList";
5
+ import ViewabilityHelper from "../viewability/ViewabilityHelper";
6
+
7
+ describe("ViewabilityHelper", () => {
8
+ const viewableIndicesChanged = jest.fn();
9
+ beforeEach(() => {
10
+ jest.resetAllMocks();
11
+ jest.useFakeTimers();
12
+ });
13
+
14
+ it("does not report any changes when indices have not changed", () => {
15
+ const viewabilityHelper = new ViewabilityHelper(
16
+ null,
17
+ viewableIndicesChanged
18
+ );
19
+ viewabilityHelper.possiblyViewableIndices = [0, 1, 2];
20
+ updateViewableItems({ viewabilityHelper });
21
+ // Initial call
22
+ expect(viewableIndicesChanged).toHaveBeenCalledWith(
23
+ [0, 1, 2],
24
+ [0, 1, 2],
25
+ []
26
+ );
27
+
28
+ // No changes
29
+ viewableIndicesChanged.mockReset();
30
+ updateViewableItems({ viewabilityHelper });
31
+ expect(viewableIndicesChanged).not.toHaveBeenCalled();
32
+ });
33
+
34
+ it("reports only viewable indices", () => {
35
+ const viewabilityHelper = new ViewabilityHelper(
36
+ null,
37
+ viewableIndicesChanged
38
+ );
39
+ viewabilityHelper.possiblyViewableIndices = [0, 1, 2, 3];
40
+ updateViewableItems({ viewabilityHelper });
41
+ // Items 0, 1, 2 are initially viewable
42
+ expect(viewableIndicesChanged).toHaveBeenCalledWith(
43
+ [0, 1, 2],
44
+ [0, 1, 2],
45
+ []
46
+ );
47
+
48
+ // After scroll, item 3 becomes viewable, too
49
+ updateViewableItems({ viewabilityHelper, scrollOffset: 50 });
50
+ expect(viewableIndicesChanged).toHaveBeenCalledWith([0, 1, 2, 3], [3], []);
51
+
52
+ // After additional scroll, the first item is no longer viewable
53
+ updateViewableItems({ viewabilityHelper, scrollOffset: 100 });
54
+ expect(viewableIndicesChanged).toHaveBeenCalledWith([1, 2, 3], [], [0]);
55
+ });
56
+
57
+ it("reports only viewable indices when horizontal", () => {
58
+ const viewabilityHelper = new ViewabilityHelper(
59
+ null,
60
+ viewableIndicesChanged
61
+ );
62
+ viewabilityHelper.possiblyViewableIndices = [0, 1, 2, 3];
63
+ const getLayout = (index: number) => {
64
+ return { x: index * 100, y: 0, height: 300, width: 100 } as Layout;
65
+ };
66
+ updateViewableItems({ viewabilityHelper, horizontal: true, getLayout });
67
+ expect(viewableIndicesChanged).toHaveBeenCalledWith(
68
+ [0, 1, 2],
69
+ [0, 1, 2],
70
+ []
71
+ );
72
+
73
+ updateViewableItems({
74
+ viewabilityHelper,
75
+ horizontal: true,
76
+ scrollOffset: 50,
77
+ getLayout,
78
+ });
79
+ expect(viewableIndicesChanged).toHaveBeenCalledWith([0, 1, 2, 3], [3], []);
80
+
81
+ updateViewableItems({
82
+ viewabilityHelper,
83
+ horizontal: true,
84
+ scrollOffset: 100,
85
+ getLayout,
86
+ });
87
+ expect(viewableIndicesChanged).toHaveBeenCalledWith([1, 2, 3], [], [0]);
88
+ });
89
+
90
+ it("reports items only after minimumViewTime has elapsed", () => {
91
+ const viewabilityHelper = new ViewabilityHelper(
92
+ { minimumViewTime: 500 },
93
+ viewableIndicesChanged
94
+ );
95
+ viewabilityHelper.possiblyViewableIndices = [0, 1, 2, 3];
96
+ updateViewableItems({ viewabilityHelper, runAllTimers: false });
97
+ expect(viewableIndicesChanged).not.toHaveBeenCalled();
98
+ jest.advanceTimersByTime(400);
99
+ expect(viewableIndicesChanged).not.toHaveBeenCalled();
100
+ jest.advanceTimersByTime(100);
101
+ expect(viewableIndicesChanged).toHaveBeenCalledWith(
102
+ [0, 1, 2],
103
+ [0, 1, 2],
104
+ []
105
+ );
106
+
107
+ viewableIndicesChanged.mockReset();
108
+ updateViewableItems({
109
+ viewabilityHelper,
110
+ scrollOffset: 50,
111
+ runAllTimers: false,
112
+ });
113
+ expect(viewableIndicesChanged).not.toHaveBeenCalled();
114
+ jest.advanceTimersByTime(500);
115
+ expect(viewableIndicesChanged).toHaveBeenCalledWith([0, 1, 2, 3], [3], []);
116
+
117
+ viewableIndicesChanged.mockReset();
118
+ updateViewableItems({
119
+ viewabilityHelper,
120
+ scrollOffset: 100,
121
+ runAllTimers: false,
122
+ });
123
+ expect(viewableIndicesChanged).not.toHaveBeenCalled();
124
+ jest.advanceTimersByTime(500);
125
+ expect(viewableIndicesChanged).toHaveBeenCalledWith([1, 2, 3], [], [0]);
126
+ });
127
+
128
+ it("reports items that only satisfy itemVisiblePercentThreshold", () => {
129
+ const viewabilityHelper = new ViewabilityHelper(
130
+ { itemVisiblePercentThreshold: 50 },
131
+ viewableIndicesChanged
132
+ );
133
+ viewabilityHelper.possiblyViewableIndices = [0, 1, 2, 3];
134
+ updateViewableItems({
135
+ viewabilityHelper,
136
+ });
137
+ expect(viewableIndicesChanged).toHaveBeenCalledWith(
138
+ [0, 1, 2],
139
+ [0, 1, 2],
140
+ []
141
+ );
142
+ viewableIndicesChanged.mockReset();
143
+
144
+ // User scrolled by 50 pixels, making both first and last item visible from 50 %
145
+ updateViewableItems({
146
+ viewabilityHelper,
147
+ scrollOffset: 50,
148
+ });
149
+ expect(viewableIndicesChanged).toHaveBeenCalledWith([0, 1, 2, 3], [3], []);
150
+ viewableIndicesChanged.mockReset();
151
+
152
+ // User scrolled by 55 pixels, first item no longer satisfies threshold
153
+ updateViewableItems({
154
+ viewabilityHelper,
155
+ scrollOffset: 55,
156
+ });
157
+ expect(viewableIndicesChanged).toHaveBeenCalledWith([1, 2, 3], [], [0]);
158
+ });
159
+
160
+ it("reports items that only satisfy viewAreaCoveragePercentThreshold", () => {
161
+ const getLayout = (index: number) => {
162
+ if (index === 4) {
163
+ return { x: 0, y: index * 100, width: 100, height: 25 } as Layout;
164
+ }
165
+ return { x: 0, y: index * 100, height: 100, width: 300 } as Layout;
166
+ };
167
+ const viewabilityHelper = new ViewabilityHelper(
168
+ { viewAreaCoveragePercentThreshold: 25 },
169
+ viewableIndicesChanged
170
+ );
171
+ viewabilityHelper.possiblyViewableIndices = [0, 1, 2, 3];
172
+ updateViewableItems({
173
+ viewabilityHelper,
174
+ getLayout,
175
+ });
176
+ expect(viewableIndicesChanged).toHaveBeenCalledWith(
177
+ [0, 1, 2],
178
+ [0, 1, 2],
179
+ []
180
+ );
181
+ viewableIndicesChanged.mockReset();
182
+
183
+ // User scrolled by 75 pixels.
184
+ // First item is visible only from 25 pixels, not meeting the threshold.
185
+ // The last item is visible from 75 pixels, which is exactly the threshold (300 / 4 = 75 where 300 is height of the list)
186
+ updateViewableItems({
187
+ viewabilityHelper,
188
+ scrollOffset: 75,
189
+ getLayout,
190
+ });
191
+ expect(viewableIndicesChanged).toHaveBeenCalledWith([1, 2, 3], [3], [0]);
192
+ viewableIndicesChanged.mockReset();
193
+
194
+ // User scrolled by 110 pixels, making the last small item only partially visible, not meeting the threshold.
195
+ viewabilityHelper.possiblyViewableIndices = [1, 2, 3, 4];
196
+ updateViewableItems({
197
+ viewabilityHelper,
198
+ scrollOffset: 110,
199
+ getLayout,
200
+ });
201
+ expect(viewableIndicesChanged).not.toHaveBeenCalled();
202
+
203
+ // User scrolled by 125 pixels, making the last small item completely visible, even when it is not meeting the threshold.
204
+ viewabilityHelper.possiblyViewableIndices = [1, 2, 3, 4];
205
+ updateViewableItems({
206
+ viewabilityHelper,
207
+ scrollOffset: 125,
208
+ getLayout,
209
+ });
210
+ expect(viewableIndicesChanged).toHaveBeenCalledWith([1, 2, 3, 4], [4], []);
211
+ });
212
+
213
+ it("reports viewable items only after interaction if waitForInteraction is set to true", () => {
214
+ const viewabilityHelper = new ViewabilityHelper(
215
+ { waitForInteraction: true },
216
+ viewableIndicesChanged
217
+ );
218
+ // Even when elements are visible, viewableIndicesChanged will not be called since interaction has not been recorded, yet
219
+ viewabilityHelper.possiblyViewableIndices = [0, 1, 2, 3];
220
+ updateViewableItems({
221
+ viewabilityHelper,
222
+ });
223
+ // View is scrolled but programatically - not resulting in an interaction
224
+ updateViewableItems({
225
+ viewabilityHelper,
226
+ scrollOffset: 50,
227
+ });
228
+ expect(viewableIndicesChanged).not.toHaveBeenCalled();
229
+
230
+ // Interaction is recorded, leading to trigger of viewableIndicesChanged
231
+ viewabilityHelper.hasInteracted = true;
232
+ updateViewableItems({
233
+ viewabilityHelper,
234
+ scrollOffset: 50,
235
+ });
236
+ expect(viewableIndicesChanged).toHaveBeenCalledWith(
237
+ [0, 1, 2, 3],
238
+ [0, 1, 2, 3],
239
+ []
240
+ );
241
+ });
242
+
243
+ it("throws multipleViewabilityThresholdTypesNotSupported exception when both viewAreaCoveragePercentThreshold and itemVisiblePercentThreshold are defined", () => {
244
+ const viewabilityHelper = new ViewabilityHelper(
245
+ { viewAreaCoveragePercentThreshold: 1, itemVisiblePercentThreshold: 1 },
246
+ viewableIndicesChanged
247
+ );
248
+ expect(() => updateViewableItems({ viewabilityHelper })).toThrow(
249
+ new CustomError(
250
+ ExceptionList.multipleViewabilityThresholdTypesNotSupported
251
+ )
252
+ );
253
+ });
254
+
255
+ const updateViewableItems = ({
256
+ viewabilityHelper,
257
+ horizontal,
258
+ scrollOffset,
259
+ listSize,
260
+ getLayout,
261
+ runAllTimers,
262
+ }: {
263
+ viewabilityHelper: ViewabilityHelper;
264
+ horizontal?: boolean;
265
+ scrollOffset?: number;
266
+ listSize?: Dimension;
267
+ getLayout?: (index: number) => Layout | undefined;
268
+ runAllTimers?: boolean;
269
+ }) => {
270
+ viewabilityHelper.updateViewableItems(
271
+ horizontal ?? false,
272
+ scrollOffset ?? 0,
273
+ listSize ?? { height: 300, width: 300 },
274
+ getLayout ??
275
+ ((index) => {
276
+ return { x: 0, y: index * 100, height: 100, width: 300 } as Layout;
277
+ })
278
+ );
279
+ if (runAllTimers ?? true) {
280
+ jest.runAllTimers();
281
+ }
282
+ };
283
+ });
@@ -0,0 +1,62 @@
1
+ import React from "react";
2
+ import { ListRenderItem, Text } from "react-native";
3
+ import "@quilted/react-testing/matchers";
4
+ import { mount, Root } from "@quilted/react-testing";
5
+
6
+ import FlashList from "../../FlashList";
7
+ import { FlashListProps } from "../../FlashListProps";
8
+
9
+ jest.mock("../../FlashList", () => {
10
+ const ActualFlashList = jest.requireActual("../../FlashList").default;
11
+ class MockFlashList extends ActualFlashList {
12
+ componentDidMount() {
13
+ super.componentDidMount();
14
+ this.rlvRef?._scrollComponent?._scrollViewRef?.props.onLayout({
15
+ nativeEvent: { layout: { height: 900, width: 400 } },
16
+ });
17
+ }
18
+ }
19
+ return MockFlashList;
20
+ });
21
+ export type MockFlashListProps = Omit<
22
+ FlashListProps<string>,
23
+ "estimatedItemSize" | "data" | "renderItem"
24
+ > & {
25
+ estimatedItemSize?: number;
26
+ data?: string[];
27
+ renderItem?: ListRenderItem<string>;
28
+ disableDefaultEstimatedItemSize?: boolean;
29
+ };
30
+ /**
31
+ * Helper to mount FlashList for testing.
32
+ */
33
+ export const mountFlashList = (
34
+ props?: MockFlashListProps,
35
+ ref?: React.RefObject<FlashList<string>>
36
+ ) => {
37
+ const flashList = mount(renderFlashList(props, ref)) as Omit<
38
+ Root<FlashListProps<string>>,
39
+ "instance"
40
+ > & {
41
+ instance: FlashList<string>;
42
+ };
43
+ return flashList;
44
+ };
45
+
46
+ export function renderFlashList(
47
+ props?: MockFlashListProps,
48
+ ref?: React.RefObject<FlashList<string>>
49
+ ) {
50
+ return (
51
+ <FlashList
52
+ {...props}
53
+ ref={ref}
54
+ renderItem={props?.renderItem || (({ item }) => <Text>{item}</Text>)}
55
+ estimatedItemSize={
56
+ props?.estimatedItemSize ??
57
+ (props?.disableDefaultEstimatedItemSize ? undefined : 200)
58
+ }
59
+ data={props?.data || ["One", "Two", "Three", "Four"]}
60
+ />
61
+ );
62
+ }