@react-native/virtualized-lists 0.72.0
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/Interaction/Batchinator.js +76 -0
- package/Interaction/__tests__/Batchinator-test.js +78 -0
- package/Lists/CellRenderMask.js +155 -0
- package/Lists/ChildListCollection.js +72 -0
- package/Lists/FillRateHelper.js +253 -0
- package/Lists/StateSafePureComponent.js +85 -0
- package/Lists/ViewabilityHelper.js +360 -0
- package/Lists/VirtualizeUtils.js +258 -0
- package/Lists/VirtualizedList.d.ts +374 -0
- package/Lists/VirtualizedList.js +1944 -0
- package/Lists/VirtualizedListCellRenderer.js +265 -0
- package/Lists/VirtualizedListContext.js +116 -0
- package/Lists/VirtualizedListProps.js +290 -0
- package/Lists/VirtualizedSectionList.js +617 -0
- package/Lists/__tests__/CellRenderMask-test.js +188 -0
- package/Lists/__tests__/FillRateHelper-test.js +120 -0
- package/Lists/__tests__/ViewabilityHelper-test.js +444 -0
- package/Lists/__tests__/VirtualizeUtils-test.js +108 -0
- package/Lists/__tests__/VirtualizedList-test.js +2208 -0
- package/Lists/__tests__/VirtualizedSectionList-test.js +278 -0
- package/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap +5837 -0
- package/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap +1200 -0
- package/Utilities/__tests__/clamp-test.js +32 -0
- package/Utilities/clamp.js +23 -0
- package/Utilities/infoLog.js +20 -0
- package/index.d.ts +10 -0
- package/index.js +57 -0
- package/package.json +21 -0
|
@@ -0,0 +1,2208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall react_native
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
import VirtualizedList from '../VirtualizedList';
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import ReactTestRenderer from 'react-test-renderer';
|
|
16
|
+
|
|
17
|
+
describe('VirtualizedList', () => {
|
|
18
|
+
it('renders simple list', () => {
|
|
19
|
+
const component = ReactTestRenderer.create(
|
|
20
|
+
<VirtualizedList
|
|
21
|
+
data={[{key: 'i1'}, {key: 'i2'}, {key: 'i3'}]}
|
|
22
|
+
renderItem={({item}) => <item value={item.key} />}
|
|
23
|
+
getItem={(data, index) => data[index]}
|
|
24
|
+
getItemCount={data => data.length}
|
|
25
|
+
/>,
|
|
26
|
+
);
|
|
27
|
+
expect(component).toMatchSnapshot();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders simple list using ListItemComponent', () => {
|
|
31
|
+
function ListItemComponent({item}) {
|
|
32
|
+
return <item value={item.key} />;
|
|
33
|
+
}
|
|
34
|
+
const component = ReactTestRenderer.create(
|
|
35
|
+
<VirtualizedList
|
|
36
|
+
data={[{key: 'i1'}, {key: 'i2'}, {key: 'i3'}]}
|
|
37
|
+
ListItemComponent={ListItemComponent}
|
|
38
|
+
getItem={(data, index) => data[index]}
|
|
39
|
+
getItemCount={data => data.length}
|
|
40
|
+
/>,
|
|
41
|
+
);
|
|
42
|
+
expect(component).toMatchSnapshot();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('warns if both renderItem or ListItemComponent are specified. Uses ListItemComponent', () => {
|
|
46
|
+
jest.spyOn(console, 'warn').mockImplementationOnce(() => {});
|
|
47
|
+
function ListItemComponent({item}) {
|
|
48
|
+
return <item value={item.key} testID={`${item.key}-ListItemComponent`} />;
|
|
49
|
+
}
|
|
50
|
+
const component = ReactTestRenderer.create(
|
|
51
|
+
<VirtualizedList
|
|
52
|
+
data={[{key: 'i1'}]}
|
|
53
|
+
ListItemComponent={ListItemComponent}
|
|
54
|
+
renderItem={({item}) => (
|
|
55
|
+
<item value={item.key} testID={`${item.key}-renderItem`} />
|
|
56
|
+
)}
|
|
57
|
+
getItem={(data, index) => data[index]}
|
|
58
|
+
getItemCount={data => data.length}
|
|
59
|
+
/>,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(console.warn).toBeCalledWith(
|
|
63
|
+
'VirtualizedList: Both ListItemComponent and renderItem props are present. ListItemComponent will take precedence over renderItem.',
|
|
64
|
+
);
|
|
65
|
+
expect(component).toMatchSnapshot();
|
|
66
|
+
console.warn.mockRestore();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('throws if no renderItem or ListItemComponent', () => {
|
|
70
|
+
// Silence the React error boundary warning; we expect an uncaught error.
|
|
71
|
+
const consoleError = console.error;
|
|
72
|
+
jest.spyOn(console, 'error').mockImplementation(message => {
|
|
73
|
+
if (message.startsWith('The above error occurred in the ')) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
consoleError(message);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const componentFactory = () =>
|
|
80
|
+
ReactTestRenderer.create(
|
|
81
|
+
<VirtualizedList
|
|
82
|
+
data={[{key: 'i1'}, {key: 'i2'}, {key: 'i3'}]}
|
|
83
|
+
getItem={(data, index) => data[index]}
|
|
84
|
+
getItemCount={data => data.length}
|
|
85
|
+
/>,
|
|
86
|
+
);
|
|
87
|
+
expect(componentFactory).toThrow(
|
|
88
|
+
'VirtualizedList: Either ListItemComponent or renderItem props are required but none were found.',
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
console.error.mockRestore();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('renders empty list', () => {
|
|
95
|
+
const component = ReactTestRenderer.create(
|
|
96
|
+
<VirtualizedList
|
|
97
|
+
data={[]}
|
|
98
|
+
renderItem={({item}) => <item value={item.key} />}
|
|
99
|
+
getItem={(data, index) => data[index]}
|
|
100
|
+
getItemCount={data => data.length}
|
|
101
|
+
/>,
|
|
102
|
+
);
|
|
103
|
+
expect(component).toMatchSnapshot();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('renders empty list after batch', () => {
|
|
107
|
+
const component = ReactTestRenderer.create(
|
|
108
|
+
<VirtualizedList
|
|
109
|
+
data={[]}
|
|
110
|
+
renderItem={({item}) => <item value={item.key} />}
|
|
111
|
+
getItem={(data, index) => data[index]}
|
|
112
|
+
getItemCount={data => data.length}
|
|
113
|
+
/>,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
ReactTestRenderer.act(() => {
|
|
117
|
+
simulateLayout(component, {
|
|
118
|
+
viewport: {width: 10, height: 50},
|
|
119
|
+
content: {width: 10, height: 200},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
performAllBatches();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(component).toMatchSnapshot();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('renders null list', () => {
|
|
129
|
+
const component = ReactTestRenderer.create(
|
|
130
|
+
<VirtualizedList
|
|
131
|
+
data={undefined}
|
|
132
|
+
renderItem={({item}) => <item value={item.key} />}
|
|
133
|
+
getItem={(data, index) => data[index]}
|
|
134
|
+
getItemCount={data => 0}
|
|
135
|
+
/>,
|
|
136
|
+
);
|
|
137
|
+
expect(component).toMatchSnapshot();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('scrollToEnd works with null list', () => {
|
|
141
|
+
const listRef = React.createRef(null);
|
|
142
|
+
ReactTestRenderer.create(
|
|
143
|
+
<VirtualizedList
|
|
144
|
+
data={undefined}
|
|
145
|
+
renderItem={({item}) => <item value={item.key} />}
|
|
146
|
+
getItem={(data, index) => data[index]}
|
|
147
|
+
getItemCount={data => 0}
|
|
148
|
+
ref={listRef}
|
|
149
|
+
/>,
|
|
150
|
+
);
|
|
151
|
+
listRef.current.scrollToEnd();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('renders empty list with empty component', () => {
|
|
155
|
+
const component = ReactTestRenderer.create(
|
|
156
|
+
<VirtualizedList
|
|
157
|
+
data={[]}
|
|
158
|
+
ListEmptyComponent={() => <empty />}
|
|
159
|
+
ListFooterComponent={() => <footer />}
|
|
160
|
+
ListHeaderComponent={() => <header />}
|
|
161
|
+
getItem={(data, index) => data[index]}
|
|
162
|
+
getItemCount={data => data.length}
|
|
163
|
+
renderItem={({item}) => <item value={item.key} />}
|
|
164
|
+
/>,
|
|
165
|
+
);
|
|
166
|
+
expect(component).toMatchSnapshot();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('renders list with empty component', () => {
|
|
170
|
+
const component = ReactTestRenderer.create(
|
|
171
|
+
<VirtualizedList
|
|
172
|
+
data={[{key: 'hello'}]}
|
|
173
|
+
ListEmptyComponent={() => <empty />}
|
|
174
|
+
getItem={(data, index) => data[index]}
|
|
175
|
+
getItemCount={data => data.length}
|
|
176
|
+
renderItem={({item}) => <item value={item.key} />}
|
|
177
|
+
/>,
|
|
178
|
+
);
|
|
179
|
+
expect(component).toMatchSnapshot();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('renders all the bells and whistles', () => {
|
|
183
|
+
const component = ReactTestRenderer.create(
|
|
184
|
+
<VirtualizedList
|
|
185
|
+
ItemSeparatorComponent={() => <separator />}
|
|
186
|
+
ListEmptyComponent={() => <empty />}
|
|
187
|
+
ListFooterComponent={() => <footer />}
|
|
188
|
+
ListHeaderComponent={() => <header />}
|
|
189
|
+
data={new Array(5).fill().map((_, ii) => ({id: String(ii)}))}
|
|
190
|
+
getItem={(data, index) => data[index]}
|
|
191
|
+
getItemCount={data => data.length}
|
|
192
|
+
getItemLayout={({index}) => ({length: 50, offset: index * 50})}
|
|
193
|
+
inverted={true}
|
|
194
|
+
keyExtractor={(item, index) => item.id}
|
|
195
|
+
onRefresh={jest.fn()}
|
|
196
|
+
refreshing={false}
|
|
197
|
+
renderItem={({item}) => <item value={item.id} />}
|
|
198
|
+
/>,
|
|
199
|
+
);
|
|
200
|
+
expect(component).toMatchSnapshot();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('test getItem functionality where data is not an Array', () => {
|
|
204
|
+
const component = ReactTestRenderer.create(
|
|
205
|
+
<VirtualizedList
|
|
206
|
+
data={new Map([['id_0', {key: 'item_0'}]])}
|
|
207
|
+
getItem={(data, index) => data.get('id_' + index)}
|
|
208
|
+
getItemCount={(data: Map) => data.size}
|
|
209
|
+
renderItem={({item}) => <item value={item.key} />}
|
|
210
|
+
/>,
|
|
211
|
+
);
|
|
212
|
+
expect(component).toMatchSnapshot();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('handles separators correctly', () => {
|
|
216
|
+
const infos = [];
|
|
217
|
+
const component = ReactTestRenderer.create(
|
|
218
|
+
<VirtualizedList
|
|
219
|
+
ItemSeparatorComponent={props => <separator {...props} />}
|
|
220
|
+
data={[{key: 'i0'}, {key: 'i1'}, {key: 'i2'}]}
|
|
221
|
+
renderItem={info => {
|
|
222
|
+
infos.push(info);
|
|
223
|
+
return <item title={info.item.key} />;
|
|
224
|
+
}}
|
|
225
|
+
getItem={(data, index) => data[index]}
|
|
226
|
+
getItemCount={data => data.length}
|
|
227
|
+
/>,
|
|
228
|
+
);
|
|
229
|
+
expect(component).toMatchSnapshot();
|
|
230
|
+
infos[1].separators.highlight();
|
|
231
|
+
expect(component).toMatchSnapshot();
|
|
232
|
+
infos[2].separators.updateProps('leading', {press: true});
|
|
233
|
+
expect(component).toMatchSnapshot();
|
|
234
|
+
infos[1].separators.unhighlight();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('handles nested lists', () => {
|
|
238
|
+
const component = ReactTestRenderer.create(
|
|
239
|
+
<VirtualizedList
|
|
240
|
+
data={[{key: 'outer0'}, {key: 'outer1'}]}
|
|
241
|
+
renderItem={outerInfo => (
|
|
242
|
+
<VirtualizedList
|
|
243
|
+
data={[
|
|
244
|
+
{key: outerInfo.item.key + ':inner0'},
|
|
245
|
+
{key: outerInfo.item.key + ':inner1'},
|
|
246
|
+
]}
|
|
247
|
+
horizontal={outerInfo.item.key === 'outer1'}
|
|
248
|
+
renderItem={innerInfo => {
|
|
249
|
+
return <item title={innerInfo.item.key} />;
|
|
250
|
+
}}
|
|
251
|
+
getItem={(data, index) => data[index]}
|
|
252
|
+
getItemCount={data => data.length}
|
|
253
|
+
/>
|
|
254
|
+
)}
|
|
255
|
+
getItem={(data, index) => data[index]}
|
|
256
|
+
getItemCount={data => data.length}
|
|
257
|
+
/>,
|
|
258
|
+
);
|
|
259
|
+
expect(component).toMatchSnapshot();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('handles nested list in ListEmptyComponent', () => {
|
|
263
|
+
const ListEmptyComponent = (
|
|
264
|
+
<VirtualizedList {...baseItemProps(generateItems(1))} />
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
let component;
|
|
268
|
+
|
|
269
|
+
ReactTestRenderer.act(() => {
|
|
270
|
+
component = ReactTestRenderer.create(
|
|
271
|
+
<VirtualizedList
|
|
272
|
+
{...baseItemProps([])}
|
|
273
|
+
ListEmptyComponent={ListEmptyComponent}
|
|
274
|
+
/>,
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
ReactTestRenderer.act(() => {
|
|
279
|
+
component.update(
|
|
280
|
+
<VirtualizedList
|
|
281
|
+
{...baseItemProps(generateItems(5))}
|
|
282
|
+
ListEmptyComponent={ListEmptyComponent}
|
|
283
|
+
/>,
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('returns the viewableItems correctly in the onViewableItemsChanged callback after changing the data', () => {
|
|
289
|
+
const ITEM_HEIGHT = 800;
|
|
290
|
+
let data = [{key: 'i1'}, {key: 'i2'}, {key: 'i3'}];
|
|
291
|
+
const nativeEvent = {
|
|
292
|
+
contentOffset: {y: 0, x: 0},
|
|
293
|
+
layoutMeasurement: {width: 300, height: 600},
|
|
294
|
+
contentSize: {width: 300, height: data.length * ITEM_HEIGHT},
|
|
295
|
+
zoomScale: 1,
|
|
296
|
+
contentInset: {right: 0, top: 0, left: 0, bottom: 0},
|
|
297
|
+
};
|
|
298
|
+
const onViewableItemsChanged = jest.fn();
|
|
299
|
+
const props = {
|
|
300
|
+
data,
|
|
301
|
+
renderItem: ({item}) => <item value={item.key} />,
|
|
302
|
+
getItem: (items, index) => items[index],
|
|
303
|
+
getItemCount: items => items.length,
|
|
304
|
+
getItemLayout: (items, index) => ({
|
|
305
|
+
length: ITEM_HEIGHT,
|
|
306
|
+
offset: ITEM_HEIGHT * index,
|
|
307
|
+
index,
|
|
308
|
+
}),
|
|
309
|
+
onViewableItemsChanged,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const component = ReactTestRenderer.create(<VirtualizedList {...props} />);
|
|
313
|
+
|
|
314
|
+
const instance = component.getInstance();
|
|
315
|
+
|
|
316
|
+
instance._onScrollBeginDrag({nativeEvent});
|
|
317
|
+
instance._onScroll({
|
|
318
|
+
timeStamp: 1000,
|
|
319
|
+
nativeEvent,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(onViewableItemsChanged).toHaveBeenCalledTimes(1);
|
|
323
|
+
expect(onViewableItemsChanged).toHaveBeenLastCalledWith(
|
|
324
|
+
expect.objectContaining({
|
|
325
|
+
viewableItems: [expect.objectContaining({isViewable: true, key: 'i1'})],
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
data = [{key: 'i4'}, ...data];
|
|
329
|
+
component.update(<VirtualizedList {...props} data={data} />);
|
|
330
|
+
|
|
331
|
+
instance._onScroll({
|
|
332
|
+
timeStamp: 2000,
|
|
333
|
+
nativeEvent: {
|
|
334
|
+
...nativeEvent,
|
|
335
|
+
contentOffset: {y: 100, x: 0},
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
expect(onViewableItemsChanged).toHaveBeenCalledTimes(2);
|
|
340
|
+
expect(onViewableItemsChanged).toHaveBeenLastCalledWith(
|
|
341
|
+
expect.objectContaining({
|
|
342
|
+
viewableItems: [expect.objectContaining({isViewable: true, key: 'i4'})],
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('getScrollRef for case where it returns a ScrollView', () => {
|
|
348
|
+
const listRef = React.createRef(null);
|
|
349
|
+
|
|
350
|
+
ReactTestRenderer.create(
|
|
351
|
+
<VirtualizedList
|
|
352
|
+
data={[{key: 'i1'}, {key: 'i2'}, {key: 'i3'}]}
|
|
353
|
+
renderItem={({item}) => <item value={item.key} />}
|
|
354
|
+
getItem={(data, index) => data[index]}
|
|
355
|
+
getItemCount={data => data.length}
|
|
356
|
+
ref={listRef}
|
|
357
|
+
/>,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const scrollRef = listRef.current.getScrollRef();
|
|
361
|
+
|
|
362
|
+
// This is checking if the ref acts like a ScrollView. If we had an
|
|
363
|
+
// `isScrollView(ref)` method, that would be preferred.
|
|
364
|
+
expect(scrollRef.scrollTo).toBeInstanceOf(jest.fn().constructor);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('getScrollRef for case where it returns a View', () => {
|
|
368
|
+
const listRef = React.createRef(null);
|
|
369
|
+
|
|
370
|
+
ReactTestRenderer.create(
|
|
371
|
+
<VirtualizedList
|
|
372
|
+
data={[{key: 'outer0'}, {key: 'outer1'}]}
|
|
373
|
+
renderItem={outerInfo => (
|
|
374
|
+
<VirtualizedList
|
|
375
|
+
data={[
|
|
376
|
+
{key: outerInfo.item.key + ':inner0'},
|
|
377
|
+
{key: outerInfo.item.key + ':inner1'},
|
|
378
|
+
]}
|
|
379
|
+
renderItem={innerInfo => {
|
|
380
|
+
return <item title={innerInfo.item.key} />;
|
|
381
|
+
}}
|
|
382
|
+
getItem={(data, index) => data[index]}
|
|
383
|
+
getItemCount={data => data.length}
|
|
384
|
+
ref={listRef}
|
|
385
|
+
/>
|
|
386
|
+
)}
|
|
387
|
+
getItem={(data, index) => data[index]}
|
|
388
|
+
getItemCount={data => data.length}
|
|
389
|
+
/>,
|
|
390
|
+
);
|
|
391
|
+
const scrollRef = listRef.current.getScrollRef();
|
|
392
|
+
|
|
393
|
+
// This is checking if the ref acts like a host component. If we had an
|
|
394
|
+
// `isHostComponent(ref)` method, that would be preferred.
|
|
395
|
+
expect(scrollRef.measure).toBeInstanceOf(jest.fn().constructor);
|
|
396
|
+
expect(scrollRef.measureLayout).toBeInstanceOf(jest.fn().constructor);
|
|
397
|
+
expect(scrollRef.measureInWindow).toBeInstanceOf(jest.fn().constructor);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('calls onStartReached when near the start', () => {
|
|
401
|
+
const ITEM_HEIGHT = 40;
|
|
402
|
+
const layout = {width: 300, height: 600};
|
|
403
|
+
let data = Array(40)
|
|
404
|
+
.fill()
|
|
405
|
+
.map((_, index) => ({key: `key-${index}`}));
|
|
406
|
+
const onStartReached = jest.fn();
|
|
407
|
+
const props = {
|
|
408
|
+
data,
|
|
409
|
+
initialNumToRender: 10,
|
|
410
|
+
onStartReachedThreshold: 1,
|
|
411
|
+
windowSize: 10,
|
|
412
|
+
renderItem: ({item}) => <item value={item.key} />,
|
|
413
|
+
getItem: (items, index) => items[index],
|
|
414
|
+
getItemCount: items => items.length,
|
|
415
|
+
getItemLayout: (items, index) => ({
|
|
416
|
+
length: ITEM_HEIGHT,
|
|
417
|
+
offset: ITEM_HEIGHT * index,
|
|
418
|
+
index,
|
|
419
|
+
}),
|
|
420
|
+
onStartReached,
|
|
421
|
+
initialScrollIndex: data.length - 1,
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const component = ReactTestRenderer.create(<VirtualizedList {...props} />);
|
|
425
|
+
|
|
426
|
+
const instance = component.getInstance();
|
|
427
|
+
|
|
428
|
+
instance._onLayout({nativeEvent: {layout, zoomScale: 1}});
|
|
429
|
+
instance._onContentSizeChange(300, data.length * ITEM_HEIGHT);
|
|
430
|
+
|
|
431
|
+
// Make sure onStartReached is not called initially when initialScrollIndex is set.
|
|
432
|
+
performAllBatches();
|
|
433
|
+
expect(onStartReached).not.toHaveBeenCalled();
|
|
434
|
+
|
|
435
|
+
// Scroll for a small amount and make sure onStartReached is not called.
|
|
436
|
+
instance._onScroll({
|
|
437
|
+
timeStamp: 1000,
|
|
438
|
+
nativeEvent: {
|
|
439
|
+
contentOffset: {y: (data.length - 2) * ITEM_HEIGHT, x: 0},
|
|
440
|
+
layoutMeasurement: layout,
|
|
441
|
+
contentSize: {...layout, height: data.length * ITEM_HEIGHT},
|
|
442
|
+
zoomScale: 1,
|
|
443
|
+
contentInset: {right: 0, top: 0, left: 0, bottom: 0},
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
performAllBatches();
|
|
447
|
+
expect(onStartReached).not.toHaveBeenCalled();
|
|
448
|
+
|
|
449
|
+
// Scroll to start and make sure onStartReached is called.
|
|
450
|
+
instance._onScroll({
|
|
451
|
+
timeStamp: 1000,
|
|
452
|
+
nativeEvent: {
|
|
453
|
+
contentOffset: {y: 0, x: 0},
|
|
454
|
+
layoutMeasurement: layout,
|
|
455
|
+
contentSize: {...layout, height: data.length * ITEM_HEIGHT},
|
|
456
|
+
zoomScale: 1,
|
|
457
|
+
contentInset: {right: 0, top: 0, left: 0, bottom: 0},
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
performAllBatches();
|
|
461
|
+
expect(onStartReached).toHaveBeenCalled();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('calls onStartReached initially', () => {
|
|
465
|
+
const ITEM_HEIGHT = 40;
|
|
466
|
+
const layout = {width: 300, height: 600};
|
|
467
|
+
let data = Array(40)
|
|
468
|
+
.fill()
|
|
469
|
+
.map((_, index) => ({key: `key-${index}`}));
|
|
470
|
+
const onStartReached = jest.fn();
|
|
471
|
+
const props = {
|
|
472
|
+
data,
|
|
473
|
+
initialNumToRender: 10,
|
|
474
|
+
onStartReachedThreshold: 1,
|
|
475
|
+
windowSize: 10,
|
|
476
|
+
renderItem: ({item}) => <item value={item.key} />,
|
|
477
|
+
getItem: (items, index) => items[index],
|
|
478
|
+
getItemCount: items => items.length,
|
|
479
|
+
getItemLayout: (items, index) => ({
|
|
480
|
+
length: ITEM_HEIGHT,
|
|
481
|
+
offset: ITEM_HEIGHT * index,
|
|
482
|
+
index,
|
|
483
|
+
}),
|
|
484
|
+
onStartReached,
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const component = ReactTestRenderer.create(<VirtualizedList {...props} />);
|
|
488
|
+
|
|
489
|
+
const instance = component.getInstance();
|
|
490
|
+
|
|
491
|
+
instance._onLayout({nativeEvent: {layout, zoomScale: 1}});
|
|
492
|
+
instance._onContentSizeChange(300, data.length * ITEM_HEIGHT);
|
|
493
|
+
|
|
494
|
+
performAllBatches();
|
|
495
|
+
expect(onStartReached).toHaveBeenCalled();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('calls onEndReached when near the end', () => {
|
|
499
|
+
const ITEM_HEIGHT = 40;
|
|
500
|
+
const layout = {width: 300, height: 600};
|
|
501
|
+
let data = Array(40)
|
|
502
|
+
.fill()
|
|
503
|
+
.map((_, index) => ({key: `key-${index}`}));
|
|
504
|
+
const onEndReached = jest.fn();
|
|
505
|
+
const props = {
|
|
506
|
+
data,
|
|
507
|
+
initialNumToRender: 10,
|
|
508
|
+
onEndReachedThreshold: 1,
|
|
509
|
+
windowSize: 10,
|
|
510
|
+
renderItem: ({item}) => <item value={item.key} />,
|
|
511
|
+
getItem: (items, index) => items[index],
|
|
512
|
+
getItemCount: items => items.length,
|
|
513
|
+
getItemLayout: (items, index) => ({
|
|
514
|
+
length: ITEM_HEIGHT,
|
|
515
|
+
offset: ITEM_HEIGHT * index,
|
|
516
|
+
index,
|
|
517
|
+
}),
|
|
518
|
+
onEndReached,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const component = ReactTestRenderer.create(<VirtualizedList {...props} />);
|
|
522
|
+
|
|
523
|
+
const instance = component.getInstance();
|
|
524
|
+
|
|
525
|
+
instance._onLayout({nativeEvent: {layout, zoomScale: 1}});
|
|
526
|
+
instance._onContentSizeChange(300, data.length * ITEM_HEIGHT);
|
|
527
|
+
|
|
528
|
+
// Make sure onEndReached is not called initially.
|
|
529
|
+
performAllBatches();
|
|
530
|
+
expect(onEndReached).not.toHaveBeenCalled();
|
|
531
|
+
|
|
532
|
+
// Scroll for a small amount and make sure onEndReached is not called.
|
|
533
|
+
instance._onScroll({
|
|
534
|
+
timeStamp: 1000,
|
|
535
|
+
nativeEvent: {
|
|
536
|
+
contentOffset: {y: ITEM_HEIGHT, x: 0},
|
|
537
|
+
layoutMeasurement: layout,
|
|
538
|
+
contentSize: {...layout, height: data.length * ITEM_HEIGHT},
|
|
539
|
+
zoomScale: 1,
|
|
540
|
+
contentInset: {right: 0, top: 0, left: 0, bottom: 0},
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
performAllBatches();
|
|
544
|
+
expect(onEndReached).not.toHaveBeenCalled();
|
|
545
|
+
|
|
546
|
+
// Scroll to end and make sure onEndReached is called.
|
|
547
|
+
instance._onScroll({
|
|
548
|
+
timeStamp: 1000,
|
|
549
|
+
nativeEvent: {
|
|
550
|
+
contentOffset: {y: data.length * ITEM_HEIGHT, x: 0},
|
|
551
|
+
layoutMeasurement: layout,
|
|
552
|
+
contentSize: {...layout, height: data.length * ITEM_HEIGHT},
|
|
553
|
+
zoomScale: 1,
|
|
554
|
+
contentInset: {right: 0, top: 0, left: 0, bottom: 0},
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
performAllBatches();
|
|
558
|
+
expect(onEndReached).toHaveBeenCalled();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('does not call onEndReached when onContentSizeChange happens after onLayout', () => {
|
|
562
|
+
const ITEM_HEIGHT = 40;
|
|
563
|
+
const layout = {width: 300, height: 600};
|
|
564
|
+
let data = Array(20)
|
|
565
|
+
.fill()
|
|
566
|
+
.map((_, index) => ({key: `key-${index}`}));
|
|
567
|
+
const onEndReached = jest.fn();
|
|
568
|
+
const props = {
|
|
569
|
+
data,
|
|
570
|
+
initialNumToRender: 10,
|
|
571
|
+
onEndReachedThreshold: 2,
|
|
572
|
+
windowSize: 21,
|
|
573
|
+
renderItem: ({item}) => <item value={item.key} />,
|
|
574
|
+
getItem: (items, index) => items[index],
|
|
575
|
+
getItemCount: items => items.length,
|
|
576
|
+
getItemLayout: (items, index) => ({
|
|
577
|
+
length: ITEM_HEIGHT,
|
|
578
|
+
offset: ITEM_HEIGHT * index,
|
|
579
|
+
index,
|
|
580
|
+
}),
|
|
581
|
+
onEndReached,
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const component = ReactTestRenderer.create(<VirtualizedList {...props} />);
|
|
585
|
+
|
|
586
|
+
const instance = component.getInstance();
|
|
587
|
+
|
|
588
|
+
instance._onLayout({nativeEvent: {layout, zoomScale: 1}});
|
|
589
|
+
|
|
590
|
+
const initialContentHeight = props.initialNumToRender * ITEM_HEIGHT;
|
|
591
|
+
|
|
592
|
+
// We want to test the unusual case of onContentSizeChange firing after
|
|
593
|
+
// onLayout, which can cause https://github.com/facebook/react-native/issues/16067
|
|
594
|
+
instance._onContentSizeChange(300, initialContentHeight);
|
|
595
|
+
instance._onContentSizeChange(300, data.length * ITEM_HEIGHT);
|
|
596
|
+
performAllBatches();
|
|
597
|
+
|
|
598
|
+
expect(onEndReached).not.toHaveBeenCalled();
|
|
599
|
+
|
|
600
|
+
instance._onScroll({
|
|
601
|
+
timeStamp: 1000,
|
|
602
|
+
nativeEvent: {
|
|
603
|
+
contentOffset: {y: initialContentHeight, x: 0},
|
|
604
|
+
layoutMeasurement: layout,
|
|
605
|
+
contentSize: {...layout, height: data.length * ITEM_HEIGHT},
|
|
606
|
+
zoomScale: 1,
|
|
607
|
+
contentInset: {right: 0, top: 0, left: 0, bottom: 0},
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
performAllBatches();
|
|
611
|
+
|
|
612
|
+
expect(onEndReached).toHaveBeenCalled();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('throws if using scrollToIndex with index less than 0', () => {
|
|
616
|
+
const component = ReactTestRenderer.create(
|
|
617
|
+
<VirtualizedList
|
|
618
|
+
data={[{key: 'i1'}, {key: 'i2'}, {key: 'i3'}]}
|
|
619
|
+
renderItem={({item}) => <item value={item.key} />}
|
|
620
|
+
getItem={(data, index) => data[index]}
|
|
621
|
+
getItemCount={data => data.length}
|
|
622
|
+
/>,
|
|
623
|
+
);
|
|
624
|
+
const instance = component.getInstance();
|
|
625
|
+
|
|
626
|
+
expect(() => instance.scrollToIndex({index: -1})).toThrow(
|
|
627
|
+
'scrollToIndex out of range: requested index -1 but minimum is 0',
|
|
628
|
+
);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('throws if using scrollToIndex when item length is less than 1', () => {
|
|
632
|
+
const component = ReactTestRenderer.create(
|
|
633
|
+
<VirtualizedList
|
|
634
|
+
data={[]}
|
|
635
|
+
renderItem={({item}) => <item value={item.key} />}
|
|
636
|
+
getItem={(data, index) => data[index]}
|
|
637
|
+
getItemCount={data => data.length}
|
|
638
|
+
/>,
|
|
639
|
+
);
|
|
640
|
+
const instance = component.getInstance();
|
|
641
|
+
|
|
642
|
+
expect(() => instance.scrollToIndex({index: 1})).toThrow(
|
|
643
|
+
'scrollToIndex out of range: item length 0 but minimum is 1',
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('throws if using scrollToIndex when requested index is bigger than or equal to item length', () => {
|
|
648
|
+
const component = ReactTestRenderer.create(
|
|
649
|
+
<VirtualizedList
|
|
650
|
+
data={[{key: 'i1'}, {key: 'i2'}, {key: 'i3'}]}
|
|
651
|
+
renderItem={({item}) => <item value={item.key} />}
|
|
652
|
+
getItem={(data, index) => data[index]}
|
|
653
|
+
getItemCount={data => data.length}
|
|
654
|
+
/>,
|
|
655
|
+
);
|
|
656
|
+
const instance = component.getInstance();
|
|
657
|
+
|
|
658
|
+
expect(() => instance.scrollToIndex({index: 3})).toThrow(
|
|
659
|
+
'scrollToIndex out of range: requested index 3 is out of 0 to 2',
|
|
660
|
+
);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('forwards correct stickyHeaderIndices when all in initial render window', () => {
|
|
664
|
+
const items = generateItemsStickyEveryN(10, 3);
|
|
665
|
+
const ITEM_HEIGHT = 10;
|
|
666
|
+
|
|
667
|
+
const component = ReactTestRenderer.create(
|
|
668
|
+
<VirtualizedList
|
|
669
|
+
initialNumToRender={10}
|
|
670
|
+
{...baseItemProps(items)}
|
|
671
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
672
|
+
/>,
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
// The initial render is specified to be the length of items provided.
|
|
676
|
+
// Expect that all sticky items (1 every 3) are passed to the underlying
|
|
677
|
+
// scrollview.
|
|
678
|
+
expect(component).toMatchSnapshot();
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('forwards correct stickyHeaderIndices when ListHeaderComponent present', () => {
|
|
682
|
+
const items = generateItemsStickyEveryN(10, 3);
|
|
683
|
+
const ITEM_HEIGHT = 10;
|
|
684
|
+
|
|
685
|
+
const component = ReactTestRenderer.create(
|
|
686
|
+
<VirtualizedList
|
|
687
|
+
ListHeaderComponent={() => React.createElement('Header')}
|
|
688
|
+
initialNumToRender={10}
|
|
689
|
+
{...baseItemProps(items)}
|
|
690
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
691
|
+
/>,
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
// The initial render is specified to be the length of items provided.
|
|
695
|
+
// Expect that all sticky items (1 every 3) are passed to the underlying
|
|
696
|
+
// scrollview, indices offset by 1 to account for the header component.
|
|
697
|
+
expect(component).toMatchSnapshot();
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('forwards correct stickyHeaderIndices when partially in initial render window', () => {
|
|
701
|
+
const items = generateItemsStickyEveryN(10, 3);
|
|
702
|
+
|
|
703
|
+
const ITEM_HEIGHT = 10;
|
|
704
|
+
|
|
705
|
+
const component = ReactTestRenderer.create(
|
|
706
|
+
<VirtualizedList
|
|
707
|
+
initialNumToRender={5}
|
|
708
|
+
{...baseItemProps(items)}
|
|
709
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
710
|
+
/>,
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
// The initial render is specified to be half the length of items provided.
|
|
714
|
+
// Expect that all sticky items of index < 5 are passed to the underlying
|
|
715
|
+
// scrollview.
|
|
716
|
+
expect(component).toMatchSnapshot();
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it('renders sticky headers in viewport on batched render', () => {
|
|
720
|
+
const items = generateItemsStickyEveryN(10, 3);
|
|
721
|
+
const ITEM_HEIGHT = 10;
|
|
722
|
+
|
|
723
|
+
let component;
|
|
724
|
+
ReactTestRenderer.act(() => {
|
|
725
|
+
component = ReactTestRenderer.create(
|
|
726
|
+
<VirtualizedList
|
|
727
|
+
initialNumToRender={1}
|
|
728
|
+
windowSize={1}
|
|
729
|
+
{...baseItemProps(items)}
|
|
730
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
731
|
+
/>,
|
|
732
|
+
);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
ReactTestRenderer.act(() => {
|
|
736
|
+
simulateLayout(component, {
|
|
737
|
+
viewport: {width: 10, height: 50},
|
|
738
|
+
content: {width: 10, height: 100},
|
|
739
|
+
});
|
|
740
|
+
performAllBatches();
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
// A windowSize of 1 means we will render just the viewport height (50dip).
|
|
744
|
+
// Expect 5 10dip items to eventually be rendered, with sticky headers in
|
|
745
|
+
// the first 5 propagated.
|
|
746
|
+
expect(component).toMatchSnapshot();
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it('keeps sticky headers above viewport visualized', () => {
|
|
750
|
+
const items = generateItemsStickyEveryN(20, 3);
|
|
751
|
+
const ITEM_HEIGHT = 10;
|
|
752
|
+
|
|
753
|
+
let component;
|
|
754
|
+
ReactTestRenderer.act(() => {
|
|
755
|
+
component = ReactTestRenderer.create(
|
|
756
|
+
<VirtualizedList
|
|
757
|
+
initialNumToRender={1}
|
|
758
|
+
windowSize={1}
|
|
759
|
+
{...baseItemProps(items)}
|
|
760
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
761
|
+
/>,
|
|
762
|
+
);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
ReactTestRenderer.act(() => {
|
|
766
|
+
simulateLayout(component, {
|
|
767
|
+
viewport: {width: 10, height: 50},
|
|
768
|
+
content: {width: 10, height: 200},
|
|
769
|
+
});
|
|
770
|
+
performAllBatches();
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
ReactTestRenderer.act(() => {
|
|
774
|
+
simulateScroll(component, {x: 0, y: 150});
|
|
775
|
+
performAllBatches();
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// Scroll to the bottom 50 dip (last five items) of the content. Expect the
|
|
779
|
+
// last five items to be rendered (possibly more if realization window is
|
|
780
|
+
// larger), along with the most recent sticky header above the realization
|
|
781
|
+
// region, even though they are out of the viewport window in layout
|
|
782
|
+
// coordinates. This is because they will remain rendered even once
|
|
783
|
+
// scrolled-past in layout space.
|
|
784
|
+
expect(component).toMatchSnapshot();
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it('unmounts sticky headers moved below viewport', () => {
|
|
789
|
+
const items = generateItemsStickyEveryN(20, 3);
|
|
790
|
+
const ITEM_HEIGHT = 10;
|
|
791
|
+
|
|
792
|
+
let component;
|
|
793
|
+
ReactTestRenderer.act(() => {
|
|
794
|
+
component = ReactTestRenderer.create(
|
|
795
|
+
<VirtualizedList
|
|
796
|
+
initialNumToRender={1}
|
|
797
|
+
windowSize={1}
|
|
798
|
+
{...baseItemProps(items)}
|
|
799
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
800
|
+
/>,
|
|
801
|
+
);
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
ReactTestRenderer.act(() => {
|
|
805
|
+
simulateLayout(component, {
|
|
806
|
+
viewport: {width: 10, height: 50},
|
|
807
|
+
content: {width: 10, height: 200},
|
|
808
|
+
});
|
|
809
|
+
performAllBatches();
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
ReactTestRenderer.act(() => {
|
|
813
|
+
simulateScroll(component, {x: 0, y: 150});
|
|
814
|
+
performAllBatches();
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
ReactTestRenderer.act(() => {
|
|
818
|
+
simulateScroll(component, {x: 0, y: 0});
|
|
819
|
+
performAllBatches();
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// Scroll to the bottom 50 dip (last five items) of the content, then back up
|
|
823
|
+
// to the first 5. Ensure that sticky items are unmounted once they are below
|
|
824
|
+
// the render area.
|
|
825
|
+
expect(component).toMatchSnapshot();
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('gracefully handles negaitve initialScrollIndex', () => {
|
|
829
|
+
const items = generateItems(10);
|
|
830
|
+
const ITEM_HEIGHT = 10;
|
|
831
|
+
|
|
832
|
+
const component = ReactTestRenderer.create(
|
|
833
|
+
<VirtualizedList
|
|
834
|
+
initialScrollIndex={-1}
|
|
835
|
+
initialNumToRender={4}
|
|
836
|
+
{...baseItemProps(items)}
|
|
837
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
838
|
+
/>,
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
// Existing code assumes we handle this in some way. Do something reasonable
|
|
842
|
+
// here.
|
|
843
|
+
expect(component).toMatchSnapshot();
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
it('renders offset cells in initial render when initialScrollIndex set', () => {
|
|
847
|
+
const items = generateItems(10);
|
|
848
|
+
const ITEM_HEIGHT = 10;
|
|
849
|
+
|
|
850
|
+
const component = ReactTestRenderer.create(
|
|
851
|
+
<VirtualizedList
|
|
852
|
+
initialScrollIndex={4}
|
|
853
|
+
initialNumToRender={4}
|
|
854
|
+
{...baseItemProps(items)}
|
|
855
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
856
|
+
/>,
|
|
857
|
+
);
|
|
858
|
+
|
|
859
|
+
// Check that the first render respects initialScrollIndex
|
|
860
|
+
expect(component).toMatchSnapshot();
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it('scrolls after content sizing with integer initialScrollIndex', () => {
|
|
864
|
+
const items = generateItems(10);
|
|
865
|
+
const ITEM_HEIGHT = 10;
|
|
866
|
+
|
|
867
|
+
const listRef = React.createRef(null);
|
|
868
|
+
|
|
869
|
+
const component = ReactTestRenderer.create(
|
|
870
|
+
<VirtualizedList
|
|
871
|
+
initialScrollIndex={1}
|
|
872
|
+
initialNumToRender={4}
|
|
873
|
+
ref={listRef}
|
|
874
|
+
{...baseItemProps(items)}
|
|
875
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
876
|
+
/>,
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
const {scrollTo} = listRef.current.getScrollRef();
|
|
880
|
+
|
|
881
|
+
ReactTestRenderer.act(() => {
|
|
882
|
+
simulateLayout(component, {
|
|
883
|
+
viewport: {width: 10, height: 50},
|
|
884
|
+
content: {width: 10, height: 200},
|
|
885
|
+
});
|
|
886
|
+
performAllBatches();
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
expect(scrollTo).toHaveBeenLastCalledWith({y: 10, animated: false});
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
it('scrolls after content sizing with near-zero initialScrollIndex', () => {
|
|
893
|
+
const items = generateItems(10);
|
|
894
|
+
const ITEM_HEIGHT = 10;
|
|
895
|
+
|
|
896
|
+
const listRef = React.createRef(null);
|
|
897
|
+
|
|
898
|
+
const component = ReactTestRenderer.create(
|
|
899
|
+
<VirtualizedList
|
|
900
|
+
initialScrollIndex={0.0001}
|
|
901
|
+
initialNumToRender={4}
|
|
902
|
+
ref={listRef}
|
|
903
|
+
{...baseItemProps(items)}
|
|
904
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
905
|
+
/>,
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
const {scrollTo} = listRef.current.getScrollRef();
|
|
909
|
+
|
|
910
|
+
ReactTestRenderer.act(() => {
|
|
911
|
+
simulateLayout(component, {
|
|
912
|
+
viewport: {width: 10, height: 50},
|
|
913
|
+
content: {width: 10, height: 200},
|
|
914
|
+
});
|
|
915
|
+
performAllBatches();
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
expect(scrollTo).toHaveBeenLastCalledWith({y: 0.001, animated: false});
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it('scrolls after content sizing with near-end initialScrollIndex', () => {
|
|
922
|
+
const items = generateItems(10);
|
|
923
|
+
const ITEM_HEIGHT = 10;
|
|
924
|
+
|
|
925
|
+
const listRef = React.createRef(null);
|
|
926
|
+
|
|
927
|
+
const component = ReactTestRenderer.create(
|
|
928
|
+
<VirtualizedList
|
|
929
|
+
initialScrollIndex={9.9999}
|
|
930
|
+
initialNumToRender={4}
|
|
931
|
+
ref={listRef}
|
|
932
|
+
{...baseItemProps(items)}
|
|
933
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
934
|
+
/>,
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
const {scrollTo} = listRef.current.getScrollRef();
|
|
938
|
+
|
|
939
|
+
ReactTestRenderer.act(() => {
|
|
940
|
+
simulateLayout(component, {
|
|
941
|
+
viewport: {width: 10, height: 50},
|
|
942
|
+
content: {width: 10, height: 200},
|
|
943
|
+
});
|
|
944
|
+
performAllBatches();
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
expect(scrollTo).toHaveBeenLastCalledWith({y: 99.999, animated: false});
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
it('scrolls after content sizing with fractional initialScrollIndex (getItemLayout())', () => {
|
|
951
|
+
const items = generateItems(10);
|
|
952
|
+
const itemHeights = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
953
|
+
const getItemLayout = (_, index) => ({
|
|
954
|
+
length: itemHeights[index],
|
|
955
|
+
offset: itemHeights.slice(0, index).reduce((a, b) => a + b, 0),
|
|
956
|
+
index,
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
const listRef = React.createRef(null);
|
|
960
|
+
|
|
961
|
+
const component = ReactTestRenderer.create(
|
|
962
|
+
<VirtualizedList
|
|
963
|
+
initialScrollIndex={1.5}
|
|
964
|
+
initialNumToRender={4}
|
|
965
|
+
ref={listRef}
|
|
966
|
+
getItemLayout={getItemLayout}
|
|
967
|
+
{...baseItemProps(items)}
|
|
968
|
+
/>,
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
const {scrollTo} = listRef.current.getScrollRef();
|
|
972
|
+
|
|
973
|
+
ReactTestRenderer.act(() => {
|
|
974
|
+
simulateLayout(component, {
|
|
975
|
+
viewport: {width: 10, height: 50},
|
|
976
|
+
content: {width: 10, height: 200},
|
|
977
|
+
});
|
|
978
|
+
performAllBatches();
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
expect(scrollTo).toHaveBeenLastCalledWith({y: 2.0, animated: false});
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
it('scrolls after content sizing with fractional initialScrollIndex (cached layout)', () => {
|
|
985
|
+
const items = generateItems(10);
|
|
986
|
+
const listRef = React.createRef(null);
|
|
987
|
+
|
|
988
|
+
const component = ReactTestRenderer.create(
|
|
989
|
+
<VirtualizedList
|
|
990
|
+
initialScrollIndex={1.5}
|
|
991
|
+
initialNumToRender={4}
|
|
992
|
+
ref={listRef}
|
|
993
|
+
{...baseItemProps(items)}
|
|
994
|
+
/>,
|
|
995
|
+
);
|
|
996
|
+
|
|
997
|
+
const {scrollTo} = listRef.current.getScrollRef();
|
|
998
|
+
|
|
999
|
+
ReactTestRenderer.act(() => {
|
|
1000
|
+
let y = 0;
|
|
1001
|
+
for (let i = 0; i < 10; ++i) {
|
|
1002
|
+
const height = i + 1;
|
|
1003
|
+
simulateCellLayout(component, items, i, {
|
|
1004
|
+
width: 10,
|
|
1005
|
+
height,
|
|
1006
|
+
x: 0,
|
|
1007
|
+
y,
|
|
1008
|
+
});
|
|
1009
|
+
y += height;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
simulateLayout(component, {
|
|
1013
|
+
viewport: {width: 10, height: 50},
|
|
1014
|
+
content: {width: 10, height: 200},
|
|
1015
|
+
});
|
|
1016
|
+
performAllBatches();
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
expect(scrollTo).toHaveBeenLastCalledWith({y: 2.0, animated: false});
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
it('scrolls after content sizing with fractional initialScrollIndex (layout estimation)', () => {
|
|
1023
|
+
const items = generateItems(10);
|
|
1024
|
+
const listRef = React.createRef(null);
|
|
1025
|
+
|
|
1026
|
+
const component = ReactTestRenderer.create(
|
|
1027
|
+
<VirtualizedList
|
|
1028
|
+
initialScrollIndex={1.5}
|
|
1029
|
+
initialNumToRender={4}
|
|
1030
|
+
ref={listRef}
|
|
1031
|
+
{...baseItemProps(items)}
|
|
1032
|
+
/>,
|
|
1033
|
+
);
|
|
1034
|
+
|
|
1035
|
+
const {scrollTo} = listRef.current.getScrollRef();
|
|
1036
|
+
|
|
1037
|
+
ReactTestRenderer.act(() => {
|
|
1038
|
+
let y = 0;
|
|
1039
|
+
for (let i = 5; i < 10; ++i) {
|
|
1040
|
+
const height = i + 1;
|
|
1041
|
+
simulateCellLayout(component, items, i, {
|
|
1042
|
+
width: 10,
|
|
1043
|
+
height,
|
|
1044
|
+
x: 0,
|
|
1045
|
+
y,
|
|
1046
|
+
});
|
|
1047
|
+
y += height;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
simulateLayout(component, {
|
|
1051
|
+
viewport: {width: 10, height: 50},
|
|
1052
|
+
content: {width: 10, height: 200},
|
|
1053
|
+
});
|
|
1054
|
+
performAllBatches();
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
expect(scrollTo).toHaveBeenLastCalledWith({y: 12, animated: false});
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
it('initially renders nothing when initialNumToRender is 0', () => {
|
|
1061
|
+
const items = generateItems(10);
|
|
1062
|
+
const ITEM_HEIGHT = 10;
|
|
1063
|
+
|
|
1064
|
+
const component = ReactTestRenderer.create(
|
|
1065
|
+
<VirtualizedList
|
|
1066
|
+
initialNumToRender={0}
|
|
1067
|
+
{...baseItemProps(items)}
|
|
1068
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1069
|
+
/>,
|
|
1070
|
+
);
|
|
1071
|
+
|
|
1072
|
+
// Only a spacer should be present (a single item is present in the legacy
|
|
1073
|
+
// implementation)
|
|
1074
|
+
expect(component).toMatchSnapshot();
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it('does not over-render when there is less than initialNumToRender cells', () => {
|
|
1078
|
+
const items = generateItems(10);
|
|
1079
|
+
const ITEM_HEIGHT = 10;
|
|
1080
|
+
|
|
1081
|
+
const component = ReactTestRenderer.create(
|
|
1082
|
+
<VirtualizedList
|
|
1083
|
+
initialScrollIndex={4}
|
|
1084
|
+
initialNumToRender={20}
|
|
1085
|
+
{...baseItemProps(items)}
|
|
1086
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1087
|
+
/>,
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
// Check that the first render clamps to the last item when intialNumToRender
|
|
1091
|
+
// goes over it.
|
|
1092
|
+
expect(component).toMatchSnapshot();
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
it('retains intitial render if initialScrollIndex == 0', () => {
|
|
1096
|
+
const items = generateItems(20);
|
|
1097
|
+
const ITEM_HEIGHT = 10;
|
|
1098
|
+
|
|
1099
|
+
let component;
|
|
1100
|
+
ReactTestRenderer.act(() => {
|
|
1101
|
+
component = ReactTestRenderer.create(
|
|
1102
|
+
<VirtualizedList
|
|
1103
|
+
initialNumToRender={5}
|
|
1104
|
+
initialScrollIndex={0}
|
|
1105
|
+
windowSize={1}
|
|
1106
|
+
{...baseItemProps(items)}
|
|
1107
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1108
|
+
/>,
|
|
1109
|
+
);
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
ReactTestRenderer.act(() => {
|
|
1113
|
+
simulateLayout(component, {
|
|
1114
|
+
viewport: {width: 10, height: 50},
|
|
1115
|
+
content: {width: 10, height: 200},
|
|
1116
|
+
});
|
|
1117
|
+
performAllBatches();
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
ReactTestRenderer.act(() => {
|
|
1121
|
+
simulateScroll(component, {x: 0, y: 150});
|
|
1122
|
+
performAllBatches();
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
// If initialScrollIndex is 0 (the default), we should never unmount the top
|
|
1126
|
+
// initialNumToRender as part of the "scroll to top optimization", even after
|
|
1127
|
+
// scrolling to the bottom five items.
|
|
1128
|
+
expect(component).toMatchSnapshot();
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
it('discards intitial render if initialScrollIndex != 0', () => {
|
|
1132
|
+
const items = generateItems(20);
|
|
1133
|
+
const ITEM_HEIGHT = 10;
|
|
1134
|
+
|
|
1135
|
+
let component;
|
|
1136
|
+
ReactTestRenderer.act(() => {
|
|
1137
|
+
component = ReactTestRenderer.create(
|
|
1138
|
+
<VirtualizedList
|
|
1139
|
+
initialScrollIndex={5}
|
|
1140
|
+
initialNumToRender={5}
|
|
1141
|
+
windowSize={1}
|
|
1142
|
+
{...baseItemProps(items)}
|
|
1143
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1144
|
+
/>,
|
|
1145
|
+
);
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
ReactTestRenderer.act(() => {
|
|
1149
|
+
simulateLayout(component, {
|
|
1150
|
+
viewport: {width: 10, height: 50},
|
|
1151
|
+
content: {width: 10, height: 200},
|
|
1152
|
+
});
|
|
1153
|
+
performAllBatches();
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
ReactTestRenderer.act(() => {
|
|
1157
|
+
simulateScroll(component, {x: 0, y: 150});
|
|
1158
|
+
performAllBatches();
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
// If initialScrollIndex is not 0, we do not enable retaining initial render
|
|
1162
|
+
// as part of "scroll to top" optimization.
|
|
1163
|
+
expect(component).toMatchSnapshot();
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
it('expands render area by maxToRenderPerBatch on tick', () => {
|
|
1167
|
+
const items = generateItems(20);
|
|
1168
|
+
const ITEM_HEIGHT = 10;
|
|
1169
|
+
|
|
1170
|
+
const props = {
|
|
1171
|
+
initialNumToRender: 5,
|
|
1172
|
+
maxToRenderPerBatch: 2,
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
let component;
|
|
1176
|
+
ReactTestRenderer.act(() => {
|
|
1177
|
+
component = ReactTestRenderer.create(
|
|
1178
|
+
<VirtualizedList
|
|
1179
|
+
{...baseItemProps(items)}
|
|
1180
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1181
|
+
{...props}
|
|
1182
|
+
/>,
|
|
1183
|
+
);
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
ReactTestRenderer.act(() => {
|
|
1187
|
+
simulateLayout(component, {
|
|
1188
|
+
viewport: {width: 10, height: 50},
|
|
1189
|
+
content: {width: 10, height: 200},
|
|
1190
|
+
});
|
|
1191
|
+
performNextBatch();
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
// We start by rendering 5 items in the initial render, but have default
|
|
1195
|
+
// windowSize, enabling eventual rendering up to 20 viewports worth of
|
|
1196
|
+
// content. We limit this to rendering 2 items per-batch via
|
|
1197
|
+
// maxToRenderPerBatch, so we should only have 7 items rendered after the
|
|
1198
|
+
// initial timer tick.
|
|
1199
|
+
expect(component).toMatchSnapshot();
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
it('does not adjust render area until content area layed out', () => {
|
|
1203
|
+
const items = generateItems(20);
|
|
1204
|
+
const ITEM_HEIGHT = 10;
|
|
1205
|
+
|
|
1206
|
+
let component;
|
|
1207
|
+
|
|
1208
|
+
ReactTestRenderer.act(() => {
|
|
1209
|
+
component = ReactTestRenderer.create(
|
|
1210
|
+
<VirtualizedList
|
|
1211
|
+
initialNumToRender={5}
|
|
1212
|
+
windowSize={10}
|
|
1213
|
+
{...baseItemProps(items)}
|
|
1214
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1215
|
+
/>,
|
|
1216
|
+
);
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
ReactTestRenderer.act(() => {
|
|
1220
|
+
simulateViewportLayout(component, {width: 10, height: 50});
|
|
1221
|
+
performAllBatches();
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
// We should not start layout-based logic to expand rendered area until
|
|
1225
|
+
// content is layed out. Expect only the 5 initial items to be rendered after
|
|
1226
|
+
// processing all batch work, even though the windowSize allows for more.
|
|
1227
|
+
expect(component).toMatchSnapshot();
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
it('does not move render area when initialScrollIndex is > 0 and offset not yet known', () => {
|
|
1231
|
+
const items = generateItems(20);
|
|
1232
|
+
const ITEM_HEIGHT = 10;
|
|
1233
|
+
|
|
1234
|
+
let component;
|
|
1235
|
+
|
|
1236
|
+
ReactTestRenderer.act(() => {
|
|
1237
|
+
component = ReactTestRenderer.create(
|
|
1238
|
+
<VirtualizedList
|
|
1239
|
+
initialNumToRender={5}
|
|
1240
|
+
initialScrollIndex={1}
|
|
1241
|
+
windowSize={10}
|
|
1242
|
+
{...baseItemProps(items)}
|
|
1243
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1244
|
+
/>,
|
|
1245
|
+
);
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
ReactTestRenderer.act(() => {
|
|
1249
|
+
simulateLayout(component, {
|
|
1250
|
+
viewport: {width: 10, height: 50},
|
|
1251
|
+
content: {width: 10, height: 100},
|
|
1252
|
+
});
|
|
1253
|
+
performAllBatches();
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
// 5 cells should be present starting at index 1, since we have not seen a
|
|
1257
|
+
// scroll event yet for current position.
|
|
1258
|
+
expect(component).toMatchSnapshot();
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
it('clamps render area when items removed for initialScrollIndex > 0 and scroller position not yet known', () => {
|
|
1262
|
+
const items = generateItems(20);
|
|
1263
|
+
const lessItems = generateItems(15);
|
|
1264
|
+
const ITEM_HEIGHT = 10;
|
|
1265
|
+
|
|
1266
|
+
let component;
|
|
1267
|
+
|
|
1268
|
+
ReactTestRenderer.act(() => {
|
|
1269
|
+
component = ReactTestRenderer.create(
|
|
1270
|
+
<VirtualizedList
|
|
1271
|
+
initialNumToRender={5}
|
|
1272
|
+
initialScrollIndex={14}
|
|
1273
|
+
windowSize={10}
|
|
1274
|
+
{...baseItemProps(items)}
|
|
1275
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1276
|
+
/>,
|
|
1277
|
+
);
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
ReactTestRenderer.act(() => {
|
|
1281
|
+
component.update(
|
|
1282
|
+
<VirtualizedList
|
|
1283
|
+
initialNumToRender={5}
|
|
1284
|
+
initialScrollIndex={14}
|
|
1285
|
+
windowSize={10}
|
|
1286
|
+
{...baseItemProps(lessItems)}
|
|
1287
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1288
|
+
/>,
|
|
1289
|
+
);
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
ReactTestRenderer.act(() => {
|
|
1293
|
+
simulateLayout(component, {
|
|
1294
|
+
viewport: {width: 10, height: 50},
|
|
1295
|
+
content: {width: 10, height: 100},
|
|
1296
|
+
});
|
|
1297
|
+
performAllBatches();
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
// The initial render range should be adjusted to not overflow the list
|
|
1301
|
+
expect(component).toMatchSnapshot();
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
it('adjusts render area with non-zero initialScrollIndex', () => {
|
|
1305
|
+
const items = generateItems(20);
|
|
1306
|
+
const ITEM_HEIGHT = 10;
|
|
1307
|
+
|
|
1308
|
+
let component;
|
|
1309
|
+
ReactTestRenderer.act(() => {
|
|
1310
|
+
component = ReactTestRenderer.create(
|
|
1311
|
+
<VirtualizedList
|
|
1312
|
+
initialNumToRender={5}
|
|
1313
|
+
initialScrollIndex={1}
|
|
1314
|
+
windowSize={10}
|
|
1315
|
+
maxToRenderPerBatch={10}
|
|
1316
|
+
{...baseItemProps(items)}
|
|
1317
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1318
|
+
/>,
|
|
1319
|
+
);
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
ReactTestRenderer.act(() => {
|
|
1323
|
+
simulateLayout(component, {
|
|
1324
|
+
viewport: {width: 10, height: 50},
|
|
1325
|
+
content: {width: 10, height: 200},
|
|
1326
|
+
});
|
|
1327
|
+
simulateScroll(component, {x: 0, y: 10}); // simulate scroll offset for initialScrollIndex
|
|
1328
|
+
|
|
1329
|
+
performAllBatches();
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
// We should expand the render area after receiving a message indcating we
|
|
1333
|
+
// arrived at initialScrollIndex.
|
|
1334
|
+
expect(component).toMatchSnapshot();
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
it('renders new items when data is updated with non-zero initialScrollIndex', () => {
|
|
1338
|
+
const items = generateItems(2);
|
|
1339
|
+
const ITEM_HEIGHT = 10;
|
|
1340
|
+
|
|
1341
|
+
let component;
|
|
1342
|
+
ReactTestRenderer.act(() => {
|
|
1343
|
+
component = ReactTestRenderer.create(
|
|
1344
|
+
<VirtualizedList
|
|
1345
|
+
initialNumToRender={5}
|
|
1346
|
+
initialScrollIndex={1}
|
|
1347
|
+
windowSize={10}
|
|
1348
|
+
maxToRenderPerBatch={10}
|
|
1349
|
+
{...baseItemProps(items)}
|
|
1350
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1351
|
+
/>,
|
|
1352
|
+
);
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
ReactTestRenderer.act(() => {
|
|
1356
|
+
simulateLayout(component, {
|
|
1357
|
+
viewport: {width: 10, height: 20},
|
|
1358
|
+
content: {width: 10, height: 20},
|
|
1359
|
+
});
|
|
1360
|
+
performAllBatches();
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
const newItems = generateItems(4);
|
|
1364
|
+
|
|
1365
|
+
ReactTestRenderer.act(() => {
|
|
1366
|
+
component.update(
|
|
1367
|
+
<VirtualizedList
|
|
1368
|
+
initialNumToRender={5}
|
|
1369
|
+
initialScrollIndex={1}
|
|
1370
|
+
windowSize={10}
|
|
1371
|
+
maxToRenderPerBatch={10}
|
|
1372
|
+
{...baseItemProps(newItems)}
|
|
1373
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1374
|
+
/>,
|
|
1375
|
+
);
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
ReactTestRenderer.act(() => {
|
|
1379
|
+
performAllBatches();
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
// We expect all the items to be rendered
|
|
1383
|
+
expect(component).toMatchSnapshot();
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
it('renders initialNumToRender cells when virtualization disabled', () => {
|
|
1387
|
+
const items = generateItems(10);
|
|
1388
|
+
const ITEM_HEIGHT = 10;
|
|
1389
|
+
|
|
1390
|
+
const component = ReactTestRenderer.create(
|
|
1391
|
+
<VirtualizedList
|
|
1392
|
+
initialNumToRender={5}
|
|
1393
|
+
initialScrollIndex={1}
|
|
1394
|
+
disableVirtualization
|
|
1395
|
+
{...baseItemProps(items)}
|
|
1396
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1397
|
+
/>,
|
|
1398
|
+
);
|
|
1399
|
+
|
|
1400
|
+
// We should render initialNumToRender items with no spacers on initial render
|
|
1401
|
+
// when virtualization is disabled
|
|
1402
|
+
expect(component).toMatchSnapshot();
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
it('renders no spacers up to initialScrollIndex on first render when virtualization disabled', () => {
|
|
1406
|
+
const items = generateItems(10);
|
|
1407
|
+
const ITEM_HEIGHT = 10;
|
|
1408
|
+
|
|
1409
|
+
let component;
|
|
1410
|
+
ReactTestRenderer.act(() => {
|
|
1411
|
+
component = ReactTestRenderer.create(
|
|
1412
|
+
<VirtualizedList
|
|
1413
|
+
initialNumToRender={2}
|
|
1414
|
+
initialScrollIndex={4}
|
|
1415
|
+
maxToRenderPerBatch={1}
|
|
1416
|
+
disableVirtualization
|
|
1417
|
+
{...baseItemProps(items)}
|
|
1418
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1419
|
+
/>,
|
|
1420
|
+
);
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
// There should be no spacers present in an offset initial render with
|
|
1424
|
+
// virtualiztion disabled. Only initialNumToRender items starting at
|
|
1425
|
+
// initialScrollIndex.
|
|
1426
|
+
expect(component).toMatchSnapshot();
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
it('expands first in viewport to render up to maxToRenderPerBatch on initial render', () => {
|
|
1430
|
+
const items = generateItems(10);
|
|
1431
|
+
const ITEM_HEIGHT = 10;
|
|
1432
|
+
|
|
1433
|
+
let component;
|
|
1434
|
+
ReactTestRenderer.act(() => {
|
|
1435
|
+
component = ReactTestRenderer.create(
|
|
1436
|
+
<VirtualizedList
|
|
1437
|
+
initialNumToRender={2}
|
|
1438
|
+
initialScrollIndex={4}
|
|
1439
|
+
maxToRenderPerBatch={10}
|
|
1440
|
+
{...baseItemProps(items)}
|
|
1441
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1442
|
+
/>,
|
|
1443
|
+
);
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
// When virtualization is disabled we may render items before initialItemIndex
|
|
1447
|
+
// if initialItemIndex + initialNumToRender < maToRenderPerBatch. Expect cells
|
|
1448
|
+
// 0-3 to be rendered in this example, even though initialScrollIndex is 4.
|
|
1449
|
+
expect(component).toMatchSnapshot();
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
it('renders items before initialScrollIndex on first batch tick when virtualization disabled', () => {
|
|
1453
|
+
const items = generateItems(10);
|
|
1454
|
+
const ITEM_HEIGHT = 10;
|
|
1455
|
+
|
|
1456
|
+
let component;
|
|
1457
|
+
ReactTestRenderer.act(() => {
|
|
1458
|
+
component = ReactTestRenderer.create(
|
|
1459
|
+
<VirtualizedList
|
|
1460
|
+
initialNumToRender={1}
|
|
1461
|
+
initialScrollIndex={5}
|
|
1462
|
+
maxToRenderPerBatch={1}
|
|
1463
|
+
disableVirtualization
|
|
1464
|
+
{...baseItemProps(items)}
|
|
1465
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1466
|
+
/>,
|
|
1467
|
+
);
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
ReactTestRenderer.act(() => {
|
|
1471
|
+
simulateLayout(component, {
|
|
1472
|
+
viewport: {width: 10, height: 50},
|
|
1473
|
+
content: {width: 10, height: 100},
|
|
1474
|
+
});
|
|
1475
|
+
performNextBatch();
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
// When virtualization is disabled, we render "maxToRenderPerBatch" items
|
|
1479
|
+
// sequentially per batch tick. Any items not yet rendered before
|
|
1480
|
+
// initialScrollIndex are currently rendered at this time. Expect the first
|
|
1481
|
+
// tick to render all items before initialScrollIndex, along with
|
|
1482
|
+
// maxToRenderPerBatch after.
|
|
1483
|
+
expect(component).toMatchSnapshot();
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
it('eventually renders all items when virtualization disabled', () => {
|
|
1487
|
+
const items = generateItems(10);
|
|
1488
|
+
const ITEM_HEIGHT = 10;
|
|
1489
|
+
|
|
1490
|
+
let component;
|
|
1491
|
+
ReactTestRenderer.act(() => {
|
|
1492
|
+
component = ReactTestRenderer.create(
|
|
1493
|
+
<VirtualizedList
|
|
1494
|
+
initialNumToRender={5}
|
|
1495
|
+
initialScrollIndex={1}
|
|
1496
|
+
windowSize={1}
|
|
1497
|
+
maxToRenderPerBatch={10}
|
|
1498
|
+
disableVirtualization
|
|
1499
|
+
{...baseItemProps(items)}
|
|
1500
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1501
|
+
/>,
|
|
1502
|
+
);
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
ReactTestRenderer.act(() => {
|
|
1506
|
+
simulateLayout(component, {
|
|
1507
|
+
viewport: {width: 10, height: 50},
|
|
1508
|
+
content: {width: 10, height: 100},
|
|
1509
|
+
});
|
|
1510
|
+
performAllBatches();
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
// After all batch ticks, all items should eventually be rendered when\
|
|
1514
|
+
// virtualization is disabled.
|
|
1515
|
+
expect(component).toMatchSnapshot();
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
it('retains initial render region when an item is appended', () => {
|
|
1519
|
+
const items = generateItems(10);
|
|
1520
|
+
const ITEM_HEIGHT = 10;
|
|
1521
|
+
|
|
1522
|
+
let component;
|
|
1523
|
+
ReactTestRenderer.act(() => {
|
|
1524
|
+
component = ReactTestRenderer.create(
|
|
1525
|
+
<VirtualizedList
|
|
1526
|
+
initialNumToRender={3}
|
|
1527
|
+
{...baseItemProps(items)}
|
|
1528
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1529
|
+
/>,
|
|
1530
|
+
);
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
ReactTestRenderer.act(() => {
|
|
1534
|
+
component.update(
|
|
1535
|
+
<VirtualizedList
|
|
1536
|
+
initialNumToRender={3}
|
|
1537
|
+
{...baseItemProps(items)}
|
|
1538
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1539
|
+
data={generateItems(11)}
|
|
1540
|
+
/>,
|
|
1541
|
+
);
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
// Adding an item to the list before batch render should keep the existing
|
|
1545
|
+
// rendered items rendered. Expect the first 3 items rendered, and a spacer
|
|
1546
|
+
// for 8 items (including the 11th, added item).
|
|
1547
|
+
expect(component).toMatchSnapshot();
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
it('retains batch render region when an item is appended', () => {
|
|
1551
|
+
const items = generateItems(10);
|
|
1552
|
+
const ITEM_HEIGHT = 10;
|
|
1553
|
+
|
|
1554
|
+
let component;
|
|
1555
|
+
ReactTestRenderer.act(() => {
|
|
1556
|
+
component = ReactTestRenderer.create(
|
|
1557
|
+
<VirtualizedList
|
|
1558
|
+
initialNumToRender={1}
|
|
1559
|
+
maxToRenderPerBatch={1}
|
|
1560
|
+
{...baseItemProps(items)}
|
|
1561
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1562
|
+
/>,
|
|
1563
|
+
);
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
ReactTestRenderer.act(() => {
|
|
1567
|
+
simulateLayout(component, {
|
|
1568
|
+
viewport: {width: 10, height: 50},
|
|
1569
|
+
content: {width: 10, height: 100},
|
|
1570
|
+
});
|
|
1571
|
+
performAllBatches();
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
jest.runAllTimers();
|
|
1575
|
+
|
|
1576
|
+
ReactTestRenderer.act(() => {
|
|
1577
|
+
component.update(
|
|
1578
|
+
<VirtualizedList
|
|
1579
|
+
initialNumToRender={1}
|
|
1580
|
+
maxToRenderPerBatch={1}
|
|
1581
|
+
{...baseItemProps(items)}
|
|
1582
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1583
|
+
data={generateItems(11)}
|
|
1584
|
+
/>,
|
|
1585
|
+
);
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
// Adding an item to the list after batch render should keep the existing
|
|
1589
|
+
// rendered items rendered. We batch render 10 items, then add an 11th. Expect
|
|
1590
|
+
// the first ten items to be present, with a spacer for the 11th until the
|
|
1591
|
+
// next batch render.
|
|
1592
|
+
expect(component).toMatchSnapshot();
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
it('constrains batch render region when an item is removed', () => {
|
|
1596
|
+
const items = generateItems(10);
|
|
1597
|
+
const ITEM_HEIGHT = 10;
|
|
1598
|
+
|
|
1599
|
+
let component;
|
|
1600
|
+
ReactTestRenderer.act(() => {
|
|
1601
|
+
component = ReactTestRenderer.create(
|
|
1602
|
+
<VirtualizedList
|
|
1603
|
+
initialNumToRender={1}
|
|
1604
|
+
maxToRenderPerBatch={1}
|
|
1605
|
+
{...baseItemProps(items)}
|
|
1606
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1607
|
+
/>,
|
|
1608
|
+
);
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
ReactTestRenderer.act(() => {
|
|
1612
|
+
simulateLayout(component, {
|
|
1613
|
+
viewport: {width: 10, height: 50},
|
|
1614
|
+
content: {width: 10, height: 100},
|
|
1615
|
+
});
|
|
1616
|
+
performAllBatches();
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
ReactTestRenderer.act(() => {
|
|
1620
|
+
component.update(
|
|
1621
|
+
<VirtualizedList
|
|
1622
|
+
initialNumToRender={1}
|
|
1623
|
+
maxToRenderPerBatch={1}
|
|
1624
|
+
{...baseItemProps(items)}
|
|
1625
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1626
|
+
data={generateItems(5)}
|
|
1627
|
+
/>,
|
|
1628
|
+
);
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
// If the number of items is reduced, we should remove the corresponding
|
|
1632
|
+
// already rendered items. Expect there to be 5 items present. New items in a
|
|
1633
|
+
// previously occupied index may also be immediately rendered.
|
|
1634
|
+
expect(component).toMatchSnapshot();
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
it('renders a zero-height tail spacer on initial render if getItemLayout not defined', () => {
|
|
1638
|
+
const items = generateItems(10);
|
|
1639
|
+
|
|
1640
|
+
const component = ReactTestRenderer.create(
|
|
1641
|
+
<VirtualizedList initialNumToRender={3} {...baseItemProps(items)} />,
|
|
1642
|
+
);
|
|
1643
|
+
|
|
1644
|
+
// Do not add space for out-of-viewport content on initial render when we do
|
|
1645
|
+
// not yet know how large it should be (no getItemLayout and cell onLayout not
|
|
1646
|
+
// yet called). Expect the tail spacer not to occupy space.
|
|
1647
|
+
expect(component).toMatchSnapshot();
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1650
|
+
it('renders zero-height tail spacer on batch render if cells not yet measured and getItemLayout not defined', () => {
|
|
1651
|
+
const items = generateItems(10);
|
|
1652
|
+
|
|
1653
|
+
let component;
|
|
1654
|
+
ReactTestRenderer.act(() => {
|
|
1655
|
+
component = ReactTestRenderer.create(
|
|
1656
|
+
<VirtualizedList
|
|
1657
|
+
initialNumToRender={3}
|
|
1658
|
+
maxToRenderPerBatch={1}
|
|
1659
|
+
windowSize={1}
|
|
1660
|
+
{...baseItemProps(items)}
|
|
1661
|
+
/>,
|
|
1662
|
+
);
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
ReactTestRenderer.act(() => {
|
|
1666
|
+
simulateLayout(component, {
|
|
1667
|
+
viewport: {width: 10, height: 50},
|
|
1668
|
+
content: {width: 10, height: 200},
|
|
1669
|
+
});
|
|
1670
|
+
performNextBatch();
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
// Do not add space for out-of-viewport content unless the cell has previously
|
|
1674
|
+
// been layed out and measurements cached. Expect the tail spacer not to
|
|
1675
|
+
// occupy space.
|
|
1676
|
+
expect(component).toMatchSnapshot();
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
it('renders tail spacer up to last measured index if getItemLayout not defined', () => {
|
|
1680
|
+
const items = generateItems(10);
|
|
1681
|
+
|
|
1682
|
+
let component;
|
|
1683
|
+
ReactTestRenderer.act(() => {
|
|
1684
|
+
component = ReactTestRenderer.create(
|
|
1685
|
+
<VirtualizedList
|
|
1686
|
+
initialNumToRender={3}
|
|
1687
|
+
maxToRenderPerBatch={1}
|
|
1688
|
+
windowSize={1}
|
|
1689
|
+
{...baseItemProps(items)}
|
|
1690
|
+
/>,
|
|
1691
|
+
);
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
ReactTestRenderer.act(() => {
|
|
1695
|
+
const LAST_MEASURED_CELL = 6;
|
|
1696
|
+
for (let i = 0; i <= LAST_MEASURED_CELL; ++i) {
|
|
1697
|
+
simulateCellLayout(component, items, i, {
|
|
1698
|
+
width: 10,
|
|
1699
|
+
height: 10,
|
|
1700
|
+
x: 0,
|
|
1701
|
+
y: 10 * i,
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
simulateLayout(component, {
|
|
1706
|
+
viewport: {width: 10, height: 50},
|
|
1707
|
+
content: {width: 10, height: 30},
|
|
1708
|
+
});
|
|
1709
|
+
performNextBatch();
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
// If cells in the out-of-viewport area have been measured, their space can be
|
|
1713
|
+
// incorporated into the tail spacer, without space for the cells we can not
|
|
1714
|
+
// measure until layout. Expect there to be a tail spacer occupying the space
|
|
1715
|
+
// for measured, but not yet rendered items (up to and including item 6).
|
|
1716
|
+
expect(component).toMatchSnapshot();
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
it('renders tail spacer up to last measured with irregular layout when getItemLayout undefined', () => {
|
|
1720
|
+
const items = generateItems(10);
|
|
1721
|
+
|
|
1722
|
+
let component;
|
|
1723
|
+
ReactTestRenderer.act(() => {
|
|
1724
|
+
component = ReactTestRenderer.create(
|
|
1725
|
+
<VirtualizedList
|
|
1726
|
+
initialNumToRender={3}
|
|
1727
|
+
maxToRenderPerBatch={1}
|
|
1728
|
+
windowSize={1}
|
|
1729
|
+
{...baseItemProps(items)}
|
|
1730
|
+
/>,
|
|
1731
|
+
);
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
ReactTestRenderer.act(() => {
|
|
1735
|
+
const LAST_MEASURED_CELL = 6;
|
|
1736
|
+
|
|
1737
|
+
let currentY = 0;
|
|
1738
|
+
for (let i = 0; i <= LAST_MEASURED_CELL; ++i) {
|
|
1739
|
+
simulateCellLayout(component, items, i, {
|
|
1740
|
+
width: 10,
|
|
1741
|
+
height: i,
|
|
1742
|
+
x: 0,
|
|
1743
|
+
y: currentY + i,
|
|
1744
|
+
});
|
|
1745
|
+
currentY += i;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
simulateLayout(component, {
|
|
1749
|
+
viewport: {width: 10, height: 50},
|
|
1750
|
+
content: {width: 10, height: 30},
|
|
1751
|
+
});
|
|
1752
|
+
performNextBatch();
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
// If cells in the out-of-viewport area have been measured, their space can be
|
|
1756
|
+
// incorporated into the tail spacer, without space for the cells we can not
|
|
1757
|
+
// measure until layout. Expect there to be a tail spacer occupying the space
|
|
1758
|
+
// for measured, but not yet rendered items (up to and including item 6).
|
|
1759
|
+
expect(component).toMatchSnapshot();
|
|
1760
|
+
});
|
|
1761
|
+
|
|
1762
|
+
it('renders full tail spacer if all cells measured', () => {
|
|
1763
|
+
const items = generateItems(10);
|
|
1764
|
+
|
|
1765
|
+
let component;
|
|
1766
|
+
ReactTestRenderer.act(() => {
|
|
1767
|
+
component = ReactTestRenderer.create(
|
|
1768
|
+
<VirtualizedList
|
|
1769
|
+
initialNumToRender={3}
|
|
1770
|
+
maxToRenderPerBatch={1}
|
|
1771
|
+
windowSize={1}
|
|
1772
|
+
{...baseItemProps(items)}
|
|
1773
|
+
/>,
|
|
1774
|
+
);
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
ReactTestRenderer.act(() => {
|
|
1778
|
+
const LAST_MEASURED_CELL = 9;
|
|
1779
|
+
for (let i = 0; i <= LAST_MEASURED_CELL; ++i) {
|
|
1780
|
+
simulateCellLayout(component, items, i, {
|
|
1781
|
+
width: 10,
|
|
1782
|
+
height: 10,
|
|
1783
|
+
x: 0,
|
|
1784
|
+
y: 10 * i,
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
simulateLayout(component, {
|
|
1789
|
+
viewport: {width: 10, height: 50},
|
|
1790
|
+
content: {width: 10, height: 30},
|
|
1791
|
+
});
|
|
1792
|
+
performNextBatch();
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
// The tail-spacer should occupy the space of all non-rendered items if all
|
|
1796
|
+
// items have been measured.
|
|
1797
|
+
expect(component).toMatchSnapshot();
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
it('renders windowSize derived region at top', () => {
|
|
1801
|
+
const items = generateItems(10);
|
|
1802
|
+
const ITEM_HEIGHT = 10;
|
|
1803
|
+
|
|
1804
|
+
let component;
|
|
1805
|
+
ReactTestRenderer.act(() => {
|
|
1806
|
+
component = ReactTestRenderer.create(
|
|
1807
|
+
<VirtualizedList
|
|
1808
|
+
initialNumToRender={1}
|
|
1809
|
+
maxToRenderPerBatch={1}
|
|
1810
|
+
windowSize={3}
|
|
1811
|
+
{...baseItemProps(items)}
|
|
1812
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1813
|
+
/>,
|
|
1814
|
+
);
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
ReactTestRenderer.act(() => {
|
|
1818
|
+
simulateLayout(component, {
|
|
1819
|
+
viewport: {width: 10, height: 20},
|
|
1820
|
+
content: {width: 10, height: 100},
|
|
1821
|
+
});
|
|
1822
|
+
performAllBatches();
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
jest.runAllTimers();
|
|
1826
|
+
// A windowSize of 3 means that we should render a viewport's worth of content
|
|
1827
|
+
// above and below the current. A 20 dip viewport at the top of the list means
|
|
1828
|
+
// we should render the top 4 10-dip items (for the current viewport, and
|
|
1829
|
+
// 20dip below).
|
|
1830
|
+
expect(component).toMatchSnapshot();
|
|
1831
|
+
});
|
|
1832
|
+
|
|
1833
|
+
it('renders windowSize derived region in middle', () => {
|
|
1834
|
+
const items = generateItems(10);
|
|
1835
|
+
const ITEM_HEIGHT = 10;
|
|
1836
|
+
|
|
1837
|
+
let component;
|
|
1838
|
+
ReactTestRenderer.act(() => {
|
|
1839
|
+
component = ReactTestRenderer.create(
|
|
1840
|
+
<VirtualizedList
|
|
1841
|
+
initialNumToRender={1}
|
|
1842
|
+
maxToRenderPerBatch={1}
|
|
1843
|
+
windowSize={3}
|
|
1844
|
+
{...baseItemProps(items)}
|
|
1845
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1846
|
+
/>,
|
|
1847
|
+
);
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
ReactTestRenderer.act(() => {
|
|
1851
|
+
simulateLayout(component, {
|
|
1852
|
+
viewport: {width: 10, height: 20},
|
|
1853
|
+
content: {width: 10, height: 100},
|
|
1854
|
+
});
|
|
1855
|
+
performAllBatches();
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
ReactTestRenderer.act(() => {
|
|
1859
|
+
simulateScroll(component, {x: 0, y: 50});
|
|
1860
|
+
performAllBatches();
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
jest.runAllTimers();
|
|
1864
|
+
// A windowSize of 3 means that we should render a viewport's worth of content
|
|
1865
|
+
// above and below the current. A 20 dip viewport in the top of the list means
|
|
1866
|
+
// we should render the 6 10-dip items (for the current viewport, 20 dip above
|
|
1867
|
+
// and below), along with retaining the top initialNumToRenderItems. We seem
|
|
1868
|
+
// to actually render 7 in the middle due to rounding at the moment.
|
|
1869
|
+
expect(component).toMatchSnapshot();
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
it('renders windowSize derived region at bottom', () => {
|
|
1873
|
+
const items = generateItems(10);
|
|
1874
|
+
const ITEM_HEIGHT = 10;
|
|
1875
|
+
|
|
1876
|
+
let component;
|
|
1877
|
+
ReactTestRenderer.act(() => {
|
|
1878
|
+
component = ReactTestRenderer.create(
|
|
1879
|
+
<VirtualizedList
|
|
1880
|
+
initialNumToRender={1}
|
|
1881
|
+
maxToRenderPerBatch={1}
|
|
1882
|
+
windowSize={3}
|
|
1883
|
+
{...baseItemProps(items)}
|
|
1884
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1885
|
+
/>,
|
|
1886
|
+
);
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
ReactTestRenderer.act(() => {
|
|
1890
|
+
simulateLayout(component, {
|
|
1891
|
+
viewport: {width: 10, height: 20},
|
|
1892
|
+
content: {width: 10, height: 100},
|
|
1893
|
+
});
|
|
1894
|
+
performAllBatches();
|
|
1895
|
+
});
|
|
1896
|
+
ReactTestRenderer.act(() => {
|
|
1897
|
+
simulateScroll(component, {x: 0, y: 80});
|
|
1898
|
+
performAllBatches();
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
jest.runAllTimers();
|
|
1902
|
+
// A windowSize of 3 means that we should render a viewport's worth of content
|
|
1903
|
+
// above and below the current. A 20 dip viewport at the bottom of the list
|
|
1904
|
+
// means we should render the bottom 4 10-dip items (for the current viewport,
|
|
1905
|
+
// and 20dip above), along with retaining the top initialNumToRenderItems. We
|
|
1906
|
+
// seem to actually render 4 at the bottom due to rounding at the moment.
|
|
1907
|
+
expect(component).toMatchSnapshot();
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
it('calls _onCellLayout properly', () => {
|
|
1911
|
+
const items = [{key: 'i1'}, {key: 'i2'}, {key: 'i3'}];
|
|
1912
|
+
const mock = jest.fn();
|
|
1913
|
+
const component = ReactTestRenderer.create(
|
|
1914
|
+
<VirtualizedList
|
|
1915
|
+
data={items}
|
|
1916
|
+
renderItem={({item}) => <item value={item.key} />}
|
|
1917
|
+
getItem={(data, index) => data[index]}
|
|
1918
|
+
getItemCount={data => data.length}
|
|
1919
|
+
/>,
|
|
1920
|
+
);
|
|
1921
|
+
const virtualList: VirtualizedList = component.getInstance();
|
|
1922
|
+
virtualList._onCellLayout = mock;
|
|
1923
|
+
component.update(
|
|
1924
|
+
<VirtualizedList
|
|
1925
|
+
data={[...items, {key: 'i4'}]}
|
|
1926
|
+
renderItem={({item}) => <item value={item.key} />}
|
|
1927
|
+
getItem={(data, index) => data[index]}
|
|
1928
|
+
getItemCount={data => data.length}
|
|
1929
|
+
/>,
|
|
1930
|
+
);
|
|
1931
|
+
const cell = virtualList._cellRefs.i4;
|
|
1932
|
+
const event = {
|
|
1933
|
+
nativeEvent: {layout: {x: 0, y: 0, width: 50, height: 50}, zoomScale: 1},
|
|
1934
|
+
};
|
|
1935
|
+
cell._onLayout(event);
|
|
1936
|
+
expect(mock).toHaveBeenCalledWith(event, 'i4', 3);
|
|
1937
|
+
expect(mock).not.toHaveBeenCalledWith(event, 'i3', 2);
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1940
|
+
it('keeps viewport below last focused rendered', () => {
|
|
1941
|
+
const items = generateItems(20);
|
|
1942
|
+
const ITEM_HEIGHT = 10;
|
|
1943
|
+
|
|
1944
|
+
let component;
|
|
1945
|
+
ReactTestRenderer.act(() => {
|
|
1946
|
+
component = ReactTestRenderer.create(
|
|
1947
|
+
<VirtualizedList
|
|
1948
|
+
initialNumToRender={1}
|
|
1949
|
+
windowSize={1}
|
|
1950
|
+
{...baseItemProps(items)}
|
|
1951
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1952
|
+
/>,
|
|
1953
|
+
);
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1956
|
+
ReactTestRenderer.act(() => {
|
|
1957
|
+
simulateLayout(component, {
|
|
1958
|
+
viewport: {width: 10, height: 50},
|
|
1959
|
+
content: {width: 10, height: 200},
|
|
1960
|
+
});
|
|
1961
|
+
|
|
1962
|
+
performAllBatches();
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
ReactTestRenderer.act(() => {
|
|
1966
|
+
component.getInstance()._onCellFocusCapture(3);
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
ReactTestRenderer.act(() => {
|
|
1970
|
+
simulateScroll(component, {x: 0, y: 150});
|
|
1971
|
+
performAllBatches();
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
// Cells 1-8 should remain rendered after scrolling to the bottom of the list
|
|
1975
|
+
expect(component).toMatchSnapshot();
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
it('virtualizes away last focused item if focus changes to a new cell', () => {
|
|
1979
|
+
const items = generateItems(20);
|
|
1980
|
+
const ITEM_HEIGHT = 10;
|
|
1981
|
+
|
|
1982
|
+
let component;
|
|
1983
|
+
ReactTestRenderer.act(() => {
|
|
1984
|
+
component = ReactTestRenderer.create(
|
|
1985
|
+
<VirtualizedList
|
|
1986
|
+
initialNumToRender={1}
|
|
1987
|
+
windowSize={1}
|
|
1988
|
+
{...baseItemProps(items)}
|
|
1989
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
1990
|
+
/>,
|
|
1991
|
+
);
|
|
1992
|
+
});
|
|
1993
|
+
|
|
1994
|
+
ReactTestRenderer.act(() => {
|
|
1995
|
+
simulateLayout(component, {
|
|
1996
|
+
viewport: {width: 10, height: 50},
|
|
1997
|
+
content: {width: 10, height: 200},
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
performAllBatches();
|
|
2001
|
+
});
|
|
2002
|
+
|
|
2003
|
+
ReactTestRenderer.act(() => {
|
|
2004
|
+
component.getInstance()._onCellFocusCapture(3);
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
ReactTestRenderer.act(() => {
|
|
2008
|
+
simulateScroll(component, {x: 0, y: 150});
|
|
2009
|
+
performAllBatches();
|
|
2010
|
+
});
|
|
2011
|
+
|
|
2012
|
+
ReactTestRenderer.act(() => {
|
|
2013
|
+
component.getInstance()._onCellFocusCapture(17);
|
|
2014
|
+
});
|
|
2015
|
+
|
|
2016
|
+
// Cells 1-8 should no longer be rendered after focus is moved to the end of
|
|
2017
|
+
// the list
|
|
2018
|
+
expect(component).toMatchSnapshot();
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
it('keeps viewport above last focused rendered', () => {
|
|
2022
|
+
const items = generateItems(20);
|
|
2023
|
+
const ITEM_HEIGHT = 10;
|
|
2024
|
+
|
|
2025
|
+
let component;
|
|
2026
|
+
ReactTestRenderer.act(() => {
|
|
2027
|
+
component = ReactTestRenderer.create(
|
|
2028
|
+
<VirtualizedList
|
|
2029
|
+
initialNumToRender={1}
|
|
2030
|
+
windowSize={1}
|
|
2031
|
+
{...baseItemProps(items)}
|
|
2032
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
2033
|
+
/>,
|
|
2034
|
+
);
|
|
2035
|
+
});
|
|
2036
|
+
|
|
2037
|
+
ReactTestRenderer.act(() => {
|
|
2038
|
+
simulateLayout(component, {
|
|
2039
|
+
viewport: {width: 10, height: 50},
|
|
2040
|
+
content: {width: 10, height: 200},
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
performAllBatches();
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
ReactTestRenderer.act(() => {
|
|
2047
|
+
component.getInstance()._onCellFocusCapture(3);
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
ReactTestRenderer.act(() => {
|
|
2051
|
+
simulateScroll(component, {x: 0, y: 150});
|
|
2052
|
+
performAllBatches();
|
|
2053
|
+
});
|
|
2054
|
+
|
|
2055
|
+
ReactTestRenderer.act(() => {
|
|
2056
|
+
component.getInstance()._onCellFocusCapture(17);
|
|
2057
|
+
});
|
|
2058
|
+
|
|
2059
|
+
ReactTestRenderer.act(() => {
|
|
2060
|
+
simulateScroll(component, {x: 0, y: 0});
|
|
2061
|
+
performAllBatches();
|
|
2062
|
+
});
|
|
2063
|
+
|
|
2064
|
+
// Cells 12-19 should remain rendered after scrolling to the top of the list
|
|
2065
|
+
expect(component).toMatchSnapshot();
|
|
2066
|
+
});
|
|
2067
|
+
|
|
2068
|
+
it('virtualizes away last focused index if item removed', () => {
|
|
2069
|
+
const items = generateItems(20);
|
|
2070
|
+
const ITEM_HEIGHT = 10;
|
|
2071
|
+
|
|
2072
|
+
let component;
|
|
2073
|
+
ReactTestRenderer.act(() => {
|
|
2074
|
+
component = ReactTestRenderer.create(
|
|
2075
|
+
<VirtualizedList
|
|
2076
|
+
initialNumToRender={1}
|
|
2077
|
+
windowSize={1}
|
|
2078
|
+
{...baseItemProps(items)}
|
|
2079
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
2080
|
+
/>,
|
|
2081
|
+
);
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
ReactTestRenderer.act(() => {
|
|
2085
|
+
simulateLayout(component, {
|
|
2086
|
+
viewport: {width: 10, height: 50},
|
|
2087
|
+
content: {width: 10, height: 200},
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
performAllBatches();
|
|
2091
|
+
});
|
|
2092
|
+
|
|
2093
|
+
ReactTestRenderer.act(() => {
|
|
2094
|
+
component.getInstance()._onCellFocusCapture(3);
|
|
2095
|
+
});
|
|
2096
|
+
|
|
2097
|
+
ReactTestRenderer.act(() => {
|
|
2098
|
+
simulateScroll(component, {x: 0, y: 150});
|
|
2099
|
+
performAllBatches();
|
|
2100
|
+
});
|
|
2101
|
+
|
|
2102
|
+
const itemsWithoutFocused = [...items.slice(0, 3), ...items.slice(4)];
|
|
2103
|
+
ReactTestRenderer.act(() => {
|
|
2104
|
+
component.update(
|
|
2105
|
+
<VirtualizedList
|
|
2106
|
+
initialNumToRender={1}
|
|
2107
|
+
windowSize={1}
|
|
2108
|
+
{...baseItemProps(itemsWithoutFocused)}
|
|
2109
|
+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
|
|
2110
|
+
/>,
|
|
2111
|
+
);
|
|
2112
|
+
});
|
|
2113
|
+
|
|
2114
|
+
// Cells 1-8 should no longer be rendered
|
|
2115
|
+
expect(component).toMatchSnapshot();
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2118
|
+
function generateItems(count) {
|
|
2119
|
+
return Array(count)
|
|
2120
|
+
.fill()
|
|
2121
|
+
.map((_, i) => ({key: i}));
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
function generateItemsStickyEveryN(count, n) {
|
|
2125
|
+
return Array(count)
|
|
2126
|
+
.fill()
|
|
2127
|
+
.map((_, i) => (i % n === 0 ? {key: i, sticky: true} : {key: i}));
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
function baseItemProps(items) {
|
|
2131
|
+
return {
|
|
2132
|
+
data: items,
|
|
2133
|
+
renderItem: ({item}) =>
|
|
2134
|
+
React.createElement('MockCellItem', {value: item.key, ...item}),
|
|
2135
|
+
getItem: (data, index) => data[index],
|
|
2136
|
+
getItemCount: data => data.length,
|
|
2137
|
+
stickyHeaderIndices: stickyHeaderIndices(items),
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
function stickyHeaderIndices(items) {
|
|
2142
|
+
return items.filter(item => item.sticky).map(item => item.key);
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
function fixedHeightItemLayoutProps(height) {
|
|
2146
|
+
return {
|
|
2147
|
+
getItemLayout: (_, index) => ({
|
|
2148
|
+
length: height,
|
|
2149
|
+
offset: height * index,
|
|
2150
|
+
index,
|
|
2151
|
+
}),
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
let lastViewportLayout;
|
|
2156
|
+
let lastContentLayout;
|
|
2157
|
+
|
|
2158
|
+
function simulateLayout(component, args) {
|
|
2159
|
+
simulateViewportLayout(component, args.viewport);
|
|
2160
|
+
simulateContentLayout(component, args.content);
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
function simulateViewportLayout(component, dimensions) {
|
|
2164
|
+
lastViewportLayout = dimensions;
|
|
2165
|
+
component
|
|
2166
|
+
.getInstance()
|
|
2167
|
+
._onLayout({nativeEvent: {layout: dimensions}, zoomScale: 1});
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
function simulateContentLayout(component, dimensions) {
|
|
2171
|
+
lastContentLayout = dimensions;
|
|
2172
|
+
component
|
|
2173
|
+
.getInstance()
|
|
2174
|
+
._onContentSizeChange(dimensions.width, dimensions.height);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
function simulateCellLayout(component, items, itemIndex, dimensions) {
|
|
2178
|
+
const instance = component.getInstance();
|
|
2179
|
+
const cellKey = instance._keyExtractor(
|
|
2180
|
+
items[itemIndex],
|
|
2181
|
+
itemIndex,
|
|
2182
|
+
instance.props,
|
|
2183
|
+
);
|
|
2184
|
+
instance._onCellLayout(
|
|
2185
|
+
{nativeEvent: {layout: dimensions, zoomScale: 1}},
|
|
2186
|
+
cellKey,
|
|
2187
|
+
itemIndex,
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function simulateScroll(component, position) {
|
|
2192
|
+
component.getInstance()._onScroll({
|
|
2193
|
+
nativeEvent: {
|
|
2194
|
+
contentOffset: position,
|
|
2195
|
+
contentSize: lastContentLayout,
|
|
2196
|
+
layoutMeasurement: lastViewportLayout,
|
|
2197
|
+
zoomScale: 1,
|
|
2198
|
+
},
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
function performAllBatches() {
|
|
2203
|
+
jest.runAllTimers();
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
function performNextBatch() {
|
|
2207
|
+
jest.runOnlyPendingTimers();
|
|
2208
|
+
}
|