@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.
@@ -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
+ }