* Copyright (c) 2024 Huawei Technologies Co., Ltd.
*
* This source code is licensed under the MIT license found in the
* LICENSE-MIT file in the root directory of this source tree.
*/
import React, { useCallback, useRef, useState } from 'react';
import {
View,
FlatList,
StyleSheet,
Text,
FlatListProps,
TouchableOpacity,
ViewToken,
ScrollViewComponent,
ScrollResponderMixin,
Platform,
} from 'react-native';
import { TestCase, TestSuite } from '@rnoh/testerino';
import { Button, ObjectDisplayer } from '../components';
interface ItemData {
title: string;
id: string;
}
const DATA: ItemData[] = [
{
id: 'gd5jc6gnbb2sbrz9w8z2',
title: 'First Item',
},
{
id: 'jb95igwbswt13etu073o',
title: 'Second Item',
},
{
id: 'zcp3zsdkkjmc7cx66hjl',
title: 'Third Item',
},
{
id: 'fx72rfguehrydmd4n21l',
title: 'Fourth Item',
},
{
id: '8kadvdlhtr7m3yv3fp4v',
title: 'Fifth Item',
},
];
type ItemProps = { title: string };
const Item = ({ title }: ItemProps) => (
<View style={styles.item}>
<Text style={styles.title}>{title}</Text>
</View>
);
const commonProps = {
style: {
height: 120,
},
data: DATA,
nestedScrollEnabled: true,
renderItem: ({ item }) => <Item title={item.title} />,
keyExtractor: item => item.id,
} satisfies FlatListProps<any>;
export const FlatListTest = () => {
return (
<TestSuite name="FlatList">
<TestCase itShould="display items in the FlatList (data, renderItem)">
<FlatList {...commonProps} />
</TestCase>
<TestCase itShould="display items with separator between them in the FlatList">
<FlatList
{...commonProps}
ItemSeparatorComponent={() => (
<View
style={{
height: 2,
alignSelf: 'center',
width: '90%',
backgroundColor: 'black',
}}
/>
)}
/>
</TestCase>
<TestCase modal itShould="render only the first two items">
<InitialNumToRenderTestCase />
</TestCase>
<TestCase modal itShould="display an array of fully visible items">
<ObjectDisplayer
renderContent={setObject => {
return <ViewabilityConfigTest setObject={setObject} />;
}}
/>
</TestCase>
<TestCase modal itShould="turn the items red on press (extraData)">
<ExtraDataTestCase />
</TestCase>
<TestCase
modal
itShould="the left list should render the added items one by one, while the right list should render almost all at once (maxToRenderPerBatch)">
<MaxToRenderPerBatchTestCase />
</TestCase>
<TestCase itShould="display empty list with a text saying that the list is empty ">
<View style={{ height: 40 }}>
<FlatList
data={[]}
nestedScrollEnabled
renderItem={() => null}
ListEmptyComponent={
<Text style={{ textAlign: 'center' }}>This list is empty</Text>
}
/>
</View>
</TestCase>
<TestCase
skip
itShould="scroll to the third item at the middle (scrollToIndex)">
<ScrollToIndexTestCase />
</TestCase>
<TestCase itShould="scroll to the third item at the middle (scrollToOffset)">
<ScrollToOffsetTestCase />
</TestCase>
<TestCase modal itShould="scroll to the third item (scrollToItem)">
<ScrollToItemTestCase />
</TestCase>
<TestCase
modal
skip={Platform.OS === 'android'}
itShould="support sticky headers (fails on Android with enabled Fabric)">
<View style={{ height: 100, backgroundColor: '#fff' }}>
<FlatList
data={DATA}
renderItem={({ item }) => (
<View
style={{
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#ccc',
}}>
<Text style={{ fontSize: 16 }}>{item.title}</Text>
</View>
)}
keyExtractor={item => item.id}
ListHeaderComponent={() => (
<View
style={{
backgroundColor: '#f0f0f0',
padding: 20,
justifyContent: 'center',
alignItems: 'center',
}}>
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>
Sticky Header
</Text>
</View>
)}
stickyHeaderIndices={[0]} // Make the header sticky
/>
</View>
</TestCase>
<TestCase
itShould="Get the node number - getScrollableNode()"
initialState={undefined}
arrange={({ state, setState }) => {
return (
<FlatListGetScrollableNode state={state} setState={setState} />
);
}}
assert={({ state, expect }) => {
expect(state).to.be.an('number');
}}
/>
<TestCase
itShould="Get the nativeScrollRef - getNativeScrollRef()"
initialState={undefined}
arrange={({ state, setState }) => {
return (
<FlatListGetNativeScrollRef state={state} setState={setState} />
);
}}
assert={({ state, expect }) => {
expect(state).to.be.not.undefined;
}}
/>
<TestCase
itShould="Get the scroll responder - getScrollResponder()"
initialState={undefined}
arrange={({ state, setState }) => {
return (
<FlatListGetScrollResponder state={state} setState={setState} />
);
}}
assert={({ state, expect }) => {
expect(state).to.be.not.undefined;
}}
/>
<TestCase
modal
skip={Platform.OS === 'android'}
itShould="stick first item to the bottom (invertStickyHeaders, fails on Android with enabled Fabric)">
<View style={{ height: 100, backgroundColor: '#fff' }}>
<FlatList
data={DATA}
renderItem={({ item }) => (
<View
style={{
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#ccc',
}}>
<Text style={{ fontSize: 16 }}>{item.title}</Text>
</View>
)}
keyExtractor={item => item.id}
ListHeaderComponent={() => (
<View
style={{
backgroundColor: '#f0f0f0',
padding: 20,
justifyContent: 'center',
alignItems: 'center',
}}>
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>Header</Text>
</View>
)}
stickyHeaderIndices={[1]}
invertStickyHeaders
/>
</View>
</TestCase>
</TestSuite>
);
};
function InitialNumToRenderTestCase() {
return (
<View
style={{
height: 120,
}}>
<FlatList
style={{
height: 120,
}}
data={DATA}
nestedScrollEnabled
renderItem={({ item }) => {
return <Item title={item.title} />;
}}
keyExtractor={item => item.id}
initialNumToRender={2}
windowSize={1}
/>
</View>
);
}
function ViewabilityConfigTest({
setObject,
}: {
setObject: (obj: Object) => void;
}) {
const viewabilityConfig = { viewAreaCoveragePercentThreshold: 100 };
const onViewableItemsChanged = useRef(
(item: { viewableItems: Array<ViewToken>; changed: Array<ViewToken> }) => {
setObject(item.viewableItems.map(i => i.item));
},
);
return (
<View style={{ height: 300 }}>
<FlatList
{...commonProps}
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={onViewableItemsChanged.current}
/>
</View>
);
}
function MaxToRenderPerBatchTestCase() {
const [data, setData] = useState<string[]>([]);
const renderItem = ({ item }: { item: string; index: number }) => {
return <Text style={{ height: 20 }}>{item}</Text>;
};
return (
<View style={{ height: 500 }}>
<View style={{ flexDirection: 'row' }}>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item, index) => index.toString()}
maxToRenderPerBatch={1}
/>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item, index) => index.toString()}
maxToRenderPerBatch={60}
/>
</View>
<Button
label="Add 60 items"
onPress={() => {
setData(prevData => [
...prevData,
...Array.from({ length: 60 }, (_, i) => `New item ${i + 1}`),
]);
}}
/>
</View>
);
}
interface SelectableListItemProps {
id: string;
onPressItem: (id: string) => void;
selected: boolean;
title: string;
}
class SelectableListItem extends React.PureComponent<SelectableListItemProps> {
_onPress = () => {
this.props.onPressItem(this.props.id);
};
render() {
const textColor = this.props.selected ? 'red' : 'black';
return (
<TouchableOpacity onPress={this._onPress}>
<View>
<Text style={{ color: textColor }}>{this.props.title}</Text>
</View>
</TouchableOpacity>
);
}
}
interface MultiSelectListState {
selected: Map<string, boolean>;
}
class ExtraDataTestCase extends React.PureComponent<{}, MultiSelectListState> {
state: MultiSelectListState = {
selected: new Map<string, boolean>(),
};
_keyExtractor = (item: ItemData, _index: number) => item.id;
_onPressItem = (id: string) => {
this.setState(state => {
const selected = new Map(state.selected);
selected.set(id, !selected.get(id));
return { selected };
});
};
_renderItem = ({ item }: { item: ItemData }) => (
<SelectableListItem
id={item.id}
onPressItem={this._onPressItem}
selected={!!this.state.selected.get(item.id)}
title={item.title}
/>
);
render() {
return (
<View style={{ height: 200 }}>
<FlatList
data={DATA}
extraData={this.state}
keyExtractor={this._keyExtractor}
renderItem={this._renderItem}
/>
</View>
);
}
}
function ScrollToIndexTestCase() {
const flatlistRef = useRef<FlatList>(null);
const [error, setError] = useState('');
return (
<>
<Button
label={'Scroll to the 3rd item at the middle'}
onPress={() => {
flatlistRef.current?.scrollToIndex({
animated: true,
index: 2,
viewPosition: 0.5,
});
}}
/>
<FlatList
{...commonProps}
ref={flatlistRef}
onScrollToIndexFailed={info => {
setError('Scroll to index failed ' + JSON.stringify(info));
}}
/>
<Text>{error}</Text>
</>
);
}
function ScrollToOffsetTestCase() {
const flatlistRef = useRef<FlatList>(null);
return (
<>
<Button
label={'Scroll to the 3rd item at top'}
onPress={() => {
flatlistRef.current?.scrollToOffset({
animated: true,
offset: 200,
});
}}
/>
<FlatList {...commonProps} ref={flatlistRef} />
</>
);
}
function ScrollToItemTestCase() {
const flatlistRef = useRef<FlatList>(null);
return (
<>
<Button
label={'Scroll to the 3rd item'}
onPress={() => {
flatlistRef.current?.scrollToItem({
animated: true,
item: DATA[2],
});
}}
/>
<FlatList {...commonProps} ref={flatlistRef} />
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
height: 120,
},
item: {
backgroundColor: '#f9c2ff',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
},
title: {
fontSize: 32,
height: 40,
width: '100%',
},
});
function FlatListGetScrollableNode({
state,
setState,
}: {
state: number | undefined;
setState: (state: any) => void;
}) {
const flatlistRef = useRef<FlatList>(null);
const getScrollableNode = useCallback(() => {
const node = flatlistRef.current?.getScrollableNode();
setState(node);
}, []);
return (
<>
<Button label={'Get Scrollable Node'} onPress={getScrollableNode} />
<FlatList {...commonProps} ref={flatlistRef} />
<Text>{state}</Text>
</>
);
}
type NativeScrollRef = React.ElementRef<typeof ScrollViewComponent>;
function FlatListGetNativeScrollRef({
setState,
}: {
state: NativeScrollRef | undefined;
setState: (state: any) => void;
}) {
const flatlistRef = useRef<FlatList>(null);
const getNativeScrollRef = useCallback(() => {
const nativeScrollRef = flatlistRef.current?.getNativeScrollRef();
setState(nativeScrollRef);
}, []);
return (
<>
<Button label={'Get nativeScrollRef'} onPress={getNativeScrollRef} />
<FlatList {...commonProps} ref={flatlistRef} />
</>
);
}
function FlatListGetScrollResponder({
setState,
}: {
state: ScrollResponderMixin | undefined;
setState: (state: any) => void;
}) {
const flatlistRef = useRef<FlatList>(null);
const getScrollResponder = useCallback(() => {
const scrollResponder = flatlistRef.current?.getScrollResponder();
setState(scrollResponder);
}, []);
return (
<>
<Button label={'Get scrollResponder'} onPress={getScrollResponder} />
<FlatList {...commonProps} ref={flatlistRef} />
</>
);
}
export default FlatListTest;