* Copyright (c) 2025 Huawei Technologies Co., Ltd.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
FlatList,
RefreshControl,
ScrollView,
StyleSheet,
Text,
View,
PanResponder,
Animated,
TouchableHighlight,
} from 'react-native';
import {TestSuite} from '@rnoh/testerino';
import React, {useCallback, useEffect, useState} from 'react';
import {Button, TestCase} from '../components';
import {useEnvironment} from '../contexts';
export const RefreshControlTest = () => {
const {
env: {driver},
} = useEnvironment();
const [refreshKey, setRefreshKey] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => setRefreshKey(prev => prev + 1), 1000);
return () => clearInterval(intervalId);
}, []);
return (
<TestSuite name="RefreshControl">
<TestCase.Example modal itShould="display refresh control every second">
<ScrollView
style={{height: 128, backgroundColor: 'white'}}
refreshControl={
<RefreshControl
refreshing={refreshKey % 2 === 0}
onRefresh={() => {}}
/>
}
/>
</TestCase.Example>
<TestCase.Example
modal
itShould="display refresh control with tintColor"
skip={{
harmony: false,
android: 'iOS specific prop',
}}>
<ScrollView
style={{height: 128, backgroundColor: 'white'}}
refreshControl={
<RefreshControl
refreshing={refreshKey % 2 === 0}
tintColor={'#FFC0CB'}
onRefresh={() => {}}
/>
}
/>
</TestCase.Example>
<TestCase.Example
modal
itShould="be refreshing for one second after pull to refresh">
<PullToRefreshExample />
</TestCase.Example>
<TestCase.Example
modal
skip={{
harmony: {
cAPI: true,
arkTs: false,
},
android: false,
}}
itShould="immediately stop refreshing after pulling to refresh">
<PullToRefreshExample doNothingOnRefresh />
</TestCase.Example>
<TestCase.Example
modal
itShould="refresh with progressViewOffset = undefined">
<PullToRefreshProgressViewOffset />
</TestCase.Example>
<TestCase.Example modal itShould="refresh with progressViewOffset = 50">
<PullToRefreshProgressViewOffset progressViewOffset={50} />
</TestCase.Example>
<TestCase.Example modal itShould="refresh with progressViewOffset = 100">
<PullToRefreshProgressViewOffset progressViewOffset={100} />
</TestCase.Example>
<TestCase.Example
skip={{
harmony: {
cAPI: true,
arkTs: false,
},
android: false,
}}
itShould="Refresh control in nested scroll view - one source of truth for both RefreshControl (one state)"
modal>
<PullToRefreshInNestedScrollViews />
</TestCase.Example>
<TestCase.Example
skip={{
harmony: {
cAPI: true,
arkTs: false,
},
android: false,
}}
itShould="Refresh control in nested scroll view - two sources of truth - one for each RefreshControl (two states)"
modal>
<PullToRefreshInNestedScrollViewsDifferentSource />
</TestCase.Example>
<TestCase.Example
skip={{
harmony: {
cAPI: true,
arkTs: false,
},
android: false,
}}
itShould="Refresh control in nested scroll view - two sources of truth - one for each RefreshControl (two states) - with the content between"
modal>
<PullToRefreshInNestedScrollViewsDifferentSourceContentBetween />
</TestCase.Example>
<TestCase.Example
skip={{
harmony: {
cAPI: true,
arkTs: false,
},
android: false,
}}
itShould="display ScrollView with green border, pink background and yellow items when RefreshControl component is set"
modal>
<RefreshControlInsideScrollViewWithStylesExample />
</TestCase.Example>
<TestCase.Example
skip={{
harmony: {
cAPI: true,
arkTs: false,
},
android: false,
}}
itShould="display FlatList with with green border, pink background and yellow items when RefreshControl component is set"
modal>
<RefreshControlInsideFlatListWithStylesExample />
</TestCase.Example>
<TestCase.Example
modal
itShould="display background color for refresh control indicator">
<ScrollView
style={{height: 128, backgroundColor: 'white'}}
refreshControl={
<RefreshControl
refreshing={refreshKey % 2 === 0}
progressBackgroundColor={'#FFC0CB'}
onRefresh={() => {}}
/>
}
/>
</TestCase.Example>
<TestCase.Example
modal
itShould="display pink 'I am refreshing!' text below refresh control indicator"
skip={{
harmony: false,
android: 'iOS specific prop',
}}>
<ScrollView
style={{height: 128, backgroundColor: 'white'}}
refreshControl={
<RefreshControl
refreshing={refreshKey % 2 === 0}
title="I am refreshing!"
titleColor={'#FFC0CB'}
onRefresh={() => {}}
/>
}
/>
</TestCase.Example>
<TestCase.Example
modal
itShould="disable/enable pull to refresh on click">
<PullToRefreshEnabledExample />
</TestCase.Example>
<TestCase.Example
modal
itShould="display a large refresh control indicator">
<ScrollView
style={{height: 128, backgroundColor: 'white'}}
refreshControl={
<RefreshControl
refreshing={refreshKey % 2 === 0}
title="I am large!"
// @ts-ignore
size={'large'}
onRefresh={() => {}}
/>
}
/>
</TestCase.Example>
<TestCase.Example
modal
itShould="Render RefreshControl on top of the scrollview">
<RefreshControlZIndex />
</TestCase.Example>
<TestCase.Example modal itShould="RefreshControlBlockNativeExample">
<RefreshControlBlockNativeExample />
</TestCase.Example>
<TestCase.Automated
itShould="detect a press during pull to refresh"
tags={['sequential']}
initialState={{targetRef: React.createRef<View>()}}
act={({state}) => {
setTimeout(() => {
driver?.click({ref: state.targetRef});
}, 500);
}}
arrange={({state, done}) => {
return (
<ScrollView
style={{height: 256}}
refreshControl={
<RefreshControl refreshing={true} onRefresh={() => {}} />
}>
<TouchableHighlight
underlayColor={'red'}
ref={state.targetRef}
style={{
width: '100%',
height: 32,
backgroundColor: 'cyan',
justifyContent: 'center',
alignItems: 'center',
}}
onPress={() => {
done();
}}>
<Text>Press Me</Text>
</TouchableHighlight>
</ScrollView>
);
}}
assert={() => {}}
/>
</TestSuite>
);
};
function PullToRefreshEnabledExample() {
const [isRefreshing, setIsRefreshing] = useState(false);
const [enabled, setEnabled] = useState<boolean | undefined>(true);
return (
<>
<Text style={{margin: 4}}>
Refresh control {enabled ? 'enabled' : 'disabled'}
</Text>
<Button
label={enabled ? 'disable' : 'enable'}
onPress={() => setEnabled(prev => !prev)}
/>
<Button label={'set undefined'} onPress={() => setEnabled(undefined)} />
<ScrollView
style={{height: 128, backgroundColor: 'white'}}
centerContent
refreshControl={
<RefreshControl
refreshing={isRefreshing}
enabled={enabled}
onRefresh={() => {
setIsRefreshing(true);
setTimeout(() => setIsRefreshing(false), 1000);
}}
/>
}>
<Text style={{width: '100%', textAlign: 'center'}}>
Pull to show refresh control
</Text>
</ScrollView>
</>
);
}
function PullToRefreshExample({
doNothingOnRefresh,
}: {
doNothingOnRefresh?: boolean;
}) {
const [isRefreshing, setIsRefreshing] = useState(false);
return (
<FlatList
style={{height: 256}}
refreshing={isRefreshing}
onRefresh={() => {
if (!doNothingOnRefresh) {
setIsRefreshing(true);
setTimeout(() => setIsRefreshing(false), 1000);
}
}}
data={[1, 2, 3, 4, 5]}
renderItem={({item}) => (
<Text style={{height: 96, borderBottomWidth: 1}}>{item}</Text>
)}
/>
);
}
function PullToRefreshProgressViewOffset({
progressViewOffset,
}: {
progressViewOffset?: number;
}) {
const [refreshing, setIsRefreshing] = useState(false);
return (
<ScrollView
style={{height: '90%', backgroundColor: 'lightgray'}}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={() => {
setIsRefreshing(true);
setTimeout(() => setIsRefreshing(false), 3000);
}}
progressViewOffset={progressViewOffset}
/>
}>
<View style={{height: 50, backgroundColor: 'lightblue'}}>
<Text>First Content Component</Text>
</View>
<View style={{height: 50, backgroundColor: 'lightgreen'}}>
<Text>Second Content Component</Text>
</View>
<View style={{height: 50, backgroundColor: 'lightblue'}}>
<Text>Third Content Component</Text>
</View>
</ScrollView>
);
}
const wait = (timeout: number) => {
return new Promise(resolve => {
setTimeout(resolve, timeout);
});
};
const PullToRefreshInNestedScrollViews = () => {
const [refreshing, setRefrehing] = useState(false);
const onRefresh = () => {
setRefrehing(true);
wait(2000).then(() => {
setRefrehing(false);
});
};
return (
<ScrollView
style={{width: '100%'}}
contentContainerStyle={[styles.scrollView]}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}>
<View
style={{
backgroundColor: 'lightblue',
width: '100%',
height: 250,
}}>
{Array.from({length: 5}).map((_, index) => (
<Text
key={index}
style={{textAlign: 'center', height: 50, borderWidth: 1}}>
{`Parent ScrollView Item ${index + 1}`}
</Text>
))}
</View>
<ScrollView
style={{width: '100%', height: 248}}
contentContainerStyle={{
width: '100%',
}}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}>
<View
style={{
backgroundColor: 'pink',
width: '100%',
}}>
{Array.from({length: 15}).map((_, index) => (
<Text
key={index}
style={{textAlign: 'center', height: 50, borderWidth: 1}}>
{`Child ScrollView Item ${index + 1}`}
</Text>
))}
</View>
</ScrollView>
</ScrollView>
);
};
const PullToRefreshInNestedScrollViewsDifferentSource = () => {
const [isRefreshingOne, setIsRefreshingOne] = useState(false);
const [isRefreshingTwo, setIsRefreshingTwo] = useState(false);
const onFirstRefresh = () => {
setIsRefreshingOne(true);
wait(2000).then(() => {
setIsRefreshingOne(false);
});
};
const onSecondRefresh = () => {
setIsRefreshingTwo(true);
wait(5000).then(() => {
setIsRefreshingTwo(false);
});
};
return (
<View>
<ScrollView
style={{width: '100%'}}
contentContainerStyle={[styles.scrollView]}
refreshControl={
<RefreshControl
refreshing={isRefreshingOne}
onRefresh={onFirstRefresh}
/>
}>
<View
style={{
backgroundColor: 'lightblue',
width: '100%',
height: 250,
}}>
{Array.from({length: 5}).map((_, index) => (
<Text
key={index}
style={{textAlign: 'center', height: 50, borderWidth: 1}}>
{`Parent ScrollView Item ${index + 1}`}
</Text>
))}
</View>
<ScrollView
style={{width: '100%', height: 248}}
contentContainerStyle={{
width: '100%',
}}
refreshControl={
<RefreshControl
refreshing={isRefreshingTwo}
onRefresh={onSecondRefresh}
/>
}>
<View
style={{
backgroundColor: 'pink',
width: '100%',
}}>
{Array.from({length: 15}).map((_, index) => (
<Text
key={index}
style={{textAlign: 'center', height: 50, borderWidth: 1}}>
{`Child ScrollView Item ${index + 1}`}
</Text>
))}
</View>
</ScrollView>
</ScrollView>
</View>
);
};
const PullToRefreshInNestedScrollViewsDifferentSourceContentBetween = () => {
const [isRefreshingOne, setIsRefreshingOne] = useState(false);
const [isRefreshingTwo, setIsRefreshingTwo] = useState(false);
const onFirstRefresh = () => {
setIsRefreshingOne(true);
wait(2000).then(() => {
setIsRefreshingOne(false);
});
};
const onSecondRefresh = () => {
setIsRefreshingTwo(true);
wait(5000).then(() => {
setIsRefreshingTwo(false);
});
};
return (
<View>
<ScrollView
style={{width: '100%'}}
contentContainerStyle={[styles.scrollView]}
refreshControl={
<RefreshControl
refreshing={isRefreshingOne}
onRefresh={onFirstRefresh}
/>
}>
<View
style={{
backgroundColor: 'lightblue',
width: '100%',
height: 250,
}}>
{Array.from({length: 5}).map((_, index) => (
<Text
key={index}
style={{textAlign: 'center', height: 50, borderWidth: 1}}>
{`Parent ScrollView Item ${index + 1}`}
</Text>
))}
</View>
<View
style={{
backgroundColor: 'red',
width: '100%',
height: 250,
justifyContent: 'center',
}}>
<Text style={{textAlign: 'center'}}>Some other content</Text>
</View>
<ScrollView
style={{width: '100%', height: 248}}
contentContainerStyle={{
width: '100%',
}}
refreshControl={
<RefreshControl
refreshing={isRefreshingTwo}
onRefresh={onSecondRefresh}
/>
}>
<View
style={{
backgroundColor: 'pink',
width: '100%',
}}>
{Array.from({length: 15}).map((_, index) => (
<Text
key={index}
style={{textAlign: 'center', height: 50, borderWidth: 1}}>
{`Child ScrollView Item ${index + 1}`}
</Text>
))}
</View>
</ScrollView>
<View
style={{
backgroundColor: 'red',
width: '100%',
height: 250,
justifyContent: 'center',
}}>
<Text style={{textAlign: 'center'}}>Some other content</Text>
</View>
<View
style={{
backgroundColor: 'blue',
width: '100%',
height: 250,
justifyContent: 'center',
}}>
<Text style={{textAlign: 'center'}}>Some other content</Text>
</View>
</ScrollView>
</View>
);
};
function RefreshControlInsideScrollViewWithStylesExample() {
const [refreshing, setRefreshing] = useState(false);
const handleRefresh = useCallback(() => {
setRefreshing(true);
wait(3000).then(() => {
setRefreshing(false);
});
}, []);
return (
<View style={{padding: 20, height: 500, backgroundColor: 'lightblue'}}>
<ScrollView
style={{
flex: 1,
backgroundColor: 'pink',
borderColor: 'green',
borderWidth: 5,
borderRadius: 50,
marginTop: 50,
paddingTop: 100,
}}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
title="I am refreshing!"
/>
}>
{Array(50)
.fill('Item')
.map((item, index) => (
<Text
style={{backgroundColor: 'yellow', padding: 10, marginBottom: 10}}
key={index}>
{item} {index}
</Text>
))}
</ScrollView>
</View>
);
}
function RefreshControlInsideFlatListWithStylesExample() {
const [refreshing, setRefreshing] = useState(false);
const handleRefresh = useCallback(() => {
setRefreshing(true);
wait(3000).then(() => {
setRefreshing(false);
});
}, []);
return (
<View style={{height: 500, padding: 20, backgroundColor: 'lightblue'}}>
<FlatList
style={{
flex: 1,
backgroundColor: 'pink',
borderColor: 'green',
borderWidth: 5,
borderRadius: 50,
marginTop: 50,
paddingTop: 100,
}}
data={Array(50).fill('Item')}
keyExtractor={(item, index) => `${item}-${index}`}
renderItem={({item, index}) => (
<Text
style={{backgroundColor: 'yellow', padding: 10, marginBottom: 10}}>
{item} {index}
</Text>
)}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
}
/>
</View>
);
}
const RefreshControlZIndex = () => {
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(() => {
setRefreshing(true);
wait(2000).then(() => setRefreshing(false));
}, []);
return (
<View style={{height: 300}}>
<ScrollView
contentContainerStyle={styles.scrollView}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
progressViewOffset={50}
/>
}>
<View style={{height: 100, width: 100, backgroundColor: 'lightblue'}}>
<Text>First Content Component</Text>
</View>
<View style={{height: 100, width: 100, backgroundColor: 'lightgreen'}}>
<Text>Second Content Component</Text>
</View>
<View style={{height: 100, width: 100, backgroundColor: 'lightblue'}}>
<Text>Third Content Component</Text>
</View>
</ScrollView>
</View>
);
};
function RefreshControlBlockNativeExample() {
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(() => {
setRefreshing(true);
wait(2000).then(() => setRefreshing(false));
}, []);
return (
<View style={{padding: 20, height: 300, backgroundColor: 'lightblue'}}>
<ScrollView
contentContainerStyle={styles.scrollView}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}>
<Text>Pull down to see RefreshControl indicator</Text>
<Animated.View
style={{marginTop: 200}}
{...PanResponder.create({
onStartShouldSetPanResponder: () => true,
onStartShouldSetPanResponderCapture: () => true,
onMoveShouldSetPanResponder: (_evt, gestureState) => {
const {dx, dy} = gestureState;
return Math.abs(dx) > 2 || Math.abs(dy) > 2;
},
onMoveShouldSetPanResponderCapture: (_evt, gestureState) => {
const {dx, dy} = gestureState;
return Math.abs(dx) > 2 || Math.abs(dy) > 2;
},
onShouldBlockNativeResponder: () => true,
}).panHandlers}>
<View
style={{
width: '70%',
height: 300,
borderWidth: 1,
overflow: 'hidden',
backgroundColor: 'yellow',
}}>
<Text style={{textAlign: 'center', fontSize: 20}}>
Sliding in the yellow area will not trigger a dropdown refresh
</Text>
</View>
</Animated.View>
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
scrollView: {
flex: 1,
width: '100%',
alignItems: 'center',
justifyContent: 'center',
},
text: {
backgroundColor: 'green',
},
});