Jest React Native, testing / mocking? onLayout - react-native

I currently have this relatively simply screen in React, that I need to test in Jest, however, I'm not terribly familiar with the library and this is what I've got so far.
Things I'd like to test?
The onLayoutEvent. Does this need mocked?
Showing / hiding of the spinner on page. At the moment, it finds it here: (which is fine)
expect(spinner).not.toBeNull();
But still finds it after the event call, whereas in actual fact it is hidden by state after the event fires.
Loading for this component occurs from a call to setLoading in it's children. I've also zero clue on how to test this. Any assistance on what / how to test this component appreciated.
describe('Login', () => {
test('it should render', () => {
renderWithStore(<LoginStarter />);
});
test('it should fire a layout event', async () => {
const { getByTestId } = renderWithStore(<LoginStarter />);
const view = await getByTestId('loginId');
const spinner = await getByTestId('loginSpinner');
expect(spinner).not.toBeNull();
act(() =>
fireEvent(view, 'onLayout', {
nativeEvent: { layout: { width: 500 } },
}),
);
});
});
The core component
const [widthLoading, setWidthLoading] = useState(true);
const [loading, setLoading] = useState(false);
const theme = useTheme();
const [containerWidth, setContainerWidth] = useState<number>();
const onLayout = (event: LayoutChangeEvent) => {
const { width } = event.nativeEvent.layout;
setContainerWidth(width);
setWidthLoading(false);
};
const tabs = [
{
title: 'Login',
component: () => LoginScreen({ componentId: props.componentId, setLoading }),
testID: 'tab1',
},
{
title: 'Sign up',
component: () => RegisterScreen({ componentId: props.componentId, setLoading }),
testID: 'tab2',
},
];
const wrapperStyle = {
flex: 1,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
opacity: loading ? 0.5 : 1,
};
const loadingStyle = {
alignItems: 'center',
justifyContent: 'center',
flex: 1,
position: 'absolute',
};
return (
<View testID="loginId" onLayout={onLayout} style={wrapperStyle as StyleProp<ViewStyle>}>
{loading && (
<View style={loadingStyle as StyleProp<ViewStyle>}>
<ActivityIndicator size="large" color={theme.spinner} />
</View>
)}
<View>
{!widthLoading && containerWidth ? (
<Tabs style="light" tabWidth={Math.round(containerWidth / 2)} tabs={tabs} tabsScrollEnabled />
) : (
<ActivityIndicator testID="loginSpinner" size="large" color={theme.spinner} />
)}
</View>
</View>
);

I'm struggling with testing onLayout as well. One thing I will point out is that in your code
act(() =>
fireEvent(view, 'onLayout', {
nativeEvent: { layout: { width: 500 } },
}),
);
onLayout should actually just be layout. I dug into the library code and found this:
const toEventHandlerName = eventName => `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`;
Also, you don't need to wrap fireEvent with act as it is wrapped by act by default. As per the docs:
Act: Useful function to help testing components that use hooks API. By default any render, update, fireEvent, and waitFor calls are wrapped by this function, so there is no need to wrap it manually.
All that said, I still can't get onLayout to fire.
Edit:
The above actually was triggering the onLayout for me. It was the was I was spying on useState that was my issue.

Related

Camera does not open ReactNative with expo

I'm having a strange problem with the camera.
I'm actually trying to open the camera but I can't, could it be because of this flex?
For example, I tried to put it outside the flex component and everything worked
const Documents = () => {
const [cameraRef, setCameraRef] = React.useState(null);
const [type, setType] = React.useState(Camera.Constants.Type.back);
const [startCamera, setStartCamera] = React.useState(false)
useEffect(() => {
(async () => {
const { status } = await Camera.requestCameraPermissionsAsync();
console.log(status)
setHasPermission(status === 'granted');
})();
}, []);
const takePhoto = async () => {
await setStartCamera(true);
console.log(startCamera);
}
return (
<><Header title={DOCUMENTS_TITLE}/><View>
<Flex h={150} alignItems="center">
<FormControl isRequired isInvalid>
<Center>
<Text
color="muted.500"
fontSize="md"
bold
italic
>Take photos of the documents</Text>
{
! startCamera ?
(
<Box>
<TouchableOpacity onPress={takePhoto}>
<Icon size="6xl" as={Feather} name="camera" color="muted.500"/>
</TouchableOpacity>
</Box>
)
:
(
<Camera
style={{flex: 10, width:"100%"}}
type={type}
ref={ref => {
setCameraRef(ref) ;
}}
></Camera>
)
}
</Center>
</FormControl>
</Flex>
</View></>
)
}
const styles = StyleSheet.create({
divider: {
width: 100
},
view: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
spacer: {
padding: 8
}
});
I don't really understand how to solve this error.
Can I do something specific?
For example, I just want to open the camera and be able to take a picture when I click on that icon described in the code and then be able to use the photo data

react native reanimated three items but one bigger as the neighbor

I want to make a flatlist with three items and the selected/current Index item should be bigger as the two items next to.
Current Code:
const translateX = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (e) => {
translateX.value = e.contentOffset.x;
}
})
const animatedStyle = useAnimatedStyle(() => {
const scale = interpolate(translateX.value, [width / 3, 0, width / 3], [0.2, 1, 0.2], Extrapolate.CLAMP);
return {
transform: [{
scale
}]
}
}, []);
const renderI = ({ item }) => (
<Animated.View style={[animatedStyle]}>
<ProductCardAuto
{...item}
onPressProduct={() => handleNavigateToProduct(item)}
onPressReviews={handleOpenModalReviews}
/>
</Animated.View>
)
<Animated.FlatList
data={mockProducts}
renderItem={renderI}
keyExtractor={(item) => item.id}
snapToAlignment="start"
decelerationRate={"normal"}
onScroll={scrollHandler}
horizontal
snapToInterval={120}
/>
I am very thankful for your help. Idk what I am doing wrong. Maybe you can tell me what is wrong with it. it does not work its scale all items to 0.2 if I scroll
Even if the question has been already answered, I would suggest another approach.
What worked for me is react-native-reanimated-carousel, which not only that is very easy to use and very customizable, but is also using react-native-reanimated and that means native performance and precise animations because it runs the animations on the UI thread rather than the JS thread.
EDIT: I noticed that you're actually looking for a reanimated answer, which is great!
Based on the library I showed you, I believe you're looking for a parallax horizontal.
Here's a link to the example: https://github.com/dohooo/react-native-reanimated-carousel/blob/main/exampleExpo/src/pages/parallax/index.tsx
kindof tried to make it acc to what you needed.
You can check this expo : https://snack.expo.dev/d_myr1HdyN
import React,{useState,useEffect,useRef,useCallback} from 'react';
import { Text, View, StyleSheet ,Animated,FlatList,Dimensions,Easing } from 'react-native';
import Constants from 'expo-constants';
// You can import from local files
import AssetExample from './components/AssetExample';
// or any pure javascript modules available in npm
import { Card } from 'react-native-paper';
const data = ["hey there","indian","mango","whatsup","arsenal","jojoba"]
const screenWidth = Dimensions.get("window").width;
const EachItem = (props) => {
const {text = "",index=0,currentView={}} = props || {}
const {visible = false, index:visibleIndex = 0} = currentView;
console.log(visible,visibleIndex,"wuhyyyyy")
const animRef = useRef(new Animated.Value(0)).current
const startAnimation = useCallback(() => {
Animated.timing(animRef,{
toValue:1,
duration:1000,
useNativeDriver:true,
easing:Easing.ease,
}).start()
},[animRef])
const stopAnim = useCallback(() => {
Animated.timing(animRef,{
toValue:0,
duration:1000,
useNativeDriver:true,
easing:Easing.ease,
}).start()
},[animRef])
useEffect(() => {
if(visible === true && visibleIndex===index ){
startAnimation()
}
if(visible === false ){
setTimeout(() =>stopAnim(),1000)
}
},[startAnimation,visible,visibleIndex,index,stopAnim])
const scaleInter = animRef.interpolate({inputRange:[0,1],outputRange:[1,1.5]})
return(
<Animated.View style={{marginHorizontal:20 ,height:100,
backgroundColor:'rgba(102,116,255,0.6)',
width:screenWidth/3,
alignItems:'center',
justifyContent:'center',
transform:[{
scale:scaleInter
}]
}} >
<Text>{text}</Text>
</Animated.View>
)
}
export default function App() {
const [currentView,setCurrentView] = useState({visible:false,index:null})
const onViewRef = React.useRef((viewableItems) => {
console.log(viewableItems?.viewableItems,"viewableItems")
if(viewableItems?.viewableItems.length === 2){
setCurrentView({visible:false,index:null})
}
if(viewableItems?.viewableItems.length === 1){
const currentItemIndex = viewableItems?.viewableItems[0]?.index
setCurrentView({visible:true,index:currentItemIndex})
}
});
const viewConfigRef = React.useRef({ viewAreaCoveragePercentThreshold: 30 });
const eachItem = ({item,index}) => {
return <EachItem text={item} currentView={currentView} index={index} />
}
return (
<View style={styles.container}>
<Animated.FlatList
data={data}
style={{flex:1}}
contentContainerStyle={{paddingTop:100}}
horizontal={true}
onViewableItemsChanged={onViewRef.current}
viewabilityConfig={viewConfigRef.current}
renderItem={eachItem}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
// justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
// alignItems:'center'
},
paragraph: {
margin: 24,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
},
});
Hope it helps. feel free for doubts

Get position of individual Items in flatlist

I have flatlist horizontal like below
const DATA = [
{
id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb28ba',
title: 'First Item',
},
{
id: '3ac68afc-c605-48d3-a4f8-fbd91aa97f63',
title: 'Second Item',
},
{
id: '58694a0f-3da1-471f-bd96-145571e29d72',
title: 'Third Item',
},
{
id: 'bd7acbea-c1b1-46c2-aed5-3ad353abb28ba',
title: 'Fourth Item',
},
{
id: '3ac68afc-c605-48d3-a4f8-fbd291aa97f63',
title: 'Fifth Item',
},
{
id: '58694a0f-3da1-471f-bd961-145571e29d72',
title: 'Sixth Item',
},
];
const Item = ({ title }) => (
<View style={styles.item}>
<Text style={styles.title}>{title}</Text>
</View>
);
const App = () => {
const renderItem = ({ item }) => (
<Item title={item.title} />
);
return (
<SafeAreaView style={styles.container}>
<FlatList
horizontal
data={DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
</SafeAreaView>
);
}
Whenever Item entered the viewport , I want to add animation to that element.I can get X and Y position of scroll with onScroll , now how do i get the positions of items to check if its in view port or if it went away from viewport...
Thank you.
Sorry for the late response. My pc has been super weird lately so when I encounter errors I have to second guess myself, and when nothing appears wrong, I second guess my pc (this time it was entirely me).
Here's my answer. I implemented the basic fade in/out [animation example][1] into the Item component. Whether it fades out or in is decided by the prop isViewable
// Item.js
const Item = (props) => {
const {
item:{title, isViewable}
} = props
/*
I copied and pasted the basic animation example from the react-native dev page
*/
const fadeAnim = useRef(new Animated.Value(1)).current;
const fadeIn = () => {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 1000,
useNativeDriver:false
}).start();
};
const fadeOut = () => {
Animated.timing(fadeAnim, {
toValue: 0,
duration: 1500,
useNativeDriver:false
}).start();
};
/* end of animation example*/
// fade in/out base on if isViewable
if(isViewable || isViewable == 0)
fadeIn()
else
fadeOut()
const animation = {opacity:fadeAnim}
return (
//add animation to Animated.View
<Animated.View style={[style.itemContainer,animation]}>
<View style={style.item}>
<Text style={style.title}>{title}</Text>
</View>
</Animated.View>
);
}
Create a FlatListWrapper (to avoid the onViewableItemChange on fly error). By doing this, as long as you don't make changes to FlatListWrapper, you wont get the on the fly error
// FlatListWrapper.js
const FlatListWrapper = (props) => {
// useRef to avoid onViewableItemsChange on fly error
const viewabilityConfig = useRef({
// useRef to try to counter the view rerender thing
itemVisiblePercentThreshold:80
}).current;
// wrapped handleViewChange in useCallback to try to handle the onViewableItemsChange on fly error
const onViewChange = useCallback(props.onViewableItemsChanged,[])
return (
<View style={style.flatlistContainer}>
<FlatList
{...props}
horizontal={true}
onViewableItemsChanged={onViewChange}
/>
</View>
);
}
const style = StyleSheet.create({
flatlistContainer:{
borderWidth:1,
borderColor:'red',
width:'50%',
height:40
},
// main FlatList component
const FlatListAnimation = () => {
// store the indices of the viewableItmes
const [ viewableItemsIndices, setViewableItemsIndices ] = useState([]);
return (
<SafeAreaView style={style.container}>
<FlatListWrapper
horizontal={true}
//{/*give each data item an isViewable prop*/}
data={DATA.map((item,i)=>{
item.isViewable=viewableItemsIndices.find(ix=>ix == i)
return item
})}
renderItem={item=><Item {...item}/>}
keyExtractor={item => item.id}
onViewableItemsChanged={({viewableItems, changed})=>{
// set viewableItemIndices to the indices when view change
setViewableItemsIndices(viewableItems.map(item=>item.index))
}}
//{/*config that decides when an item is viewable*/}
viewabilityConfig={{itemVisiblePercentThreshold:80}}
extraData={viewableItemsIndices}
/>
{/* Extra stuff that just tells you what items should be visible*/}
<Text>Items that should be visible:</Text>
{viewableItemsIndices.map(i=><Text> {DATA[i].title}</Text>)}
</SafeAreaView>
);
}
const style = StyleSheet.create({
container:{
padding:10,
alignItems:'center'
},
flatlistContainer:{
borderWidth:1,
borderColor:'red',
width:'50%',
height:40
},
item:{
borderWidth:1,
padding:5,
},
itemContainer:{
padding:5,
}
})
By wrapping your FlatList in a separate file, you wont encounter the "onViewableItemsChange on the fly" error as long as you dont modify FlatListWrapper.js
[1]: https://reactnative.dev/docs/animated
Use onViewableItemsChanged this is called when the items in the flatlist changes.
const handleViewableItemsChanged = (viewableItems, changed) => {}
<Flatlist
...
onViewableItemsChanged={handleViewableItemsChanged}

Animation won't start if state changed (short snack for demonstration)

Snack is here
Hello, I'm hard stuck on a silly problem and I'm becoming nut.
I just wanted to make a simple and elegant animation when a screen is focused (in a tab bar navigation). My snack works perfectly until I perform a state change in my screen. Then the animation just won't start, even though the callback from focus listener is called and executed (check logs)... WHY?
I made a button to trigger manually the animation... and it works!????
I think I made the snack clear, but if you need more information, please ask me. I beg you, please help a brother in despair.
Snack is here
If you're lazy to click the Snack:
import React, { useState, useEffect } from "react";
import { Text, View, Animated, Dimensions, StyleSheet, SafeAreaView, Button } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { createBottomTabNavigator } from '#react-navigation/bottom-tabs';
function HomeScreen({navigation}) {
const initialXPos = Dimensions.get("window").height * 0.5 ;
const xPos = new Animated.Value(initialXPos);
const opacity = new Animated.Value(0);
const [change, setChange] = useState(true)
useEffect(() => {
const unsubscribe = navigation.addListener("focus", comingFromBot);
return unsubscribe;
}, []);
const comingFromBot = () => {
xPos.setValue(initialXPos);
opacity.setValue(0);
Animated.parallel([
Animated.spring(xPos, {
toValue: 100,
tension:3,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
]).start();
console.log("Animation's Fired!");
};
return (
<SafeAreaView style={{flex:1}}>
<Animated.View style={[
styles.container,
{ transform: [{ translateY: xPos }] },
{ opacity: opacity },
]}>
<Text style={{fontSize:30}}>{change ? "Home!" : "TIMMY!"}</Text>
</Animated.View>
{/* debug */}
<View style={styles.fire}>
<Button title="fire" onPress={() => comingFromBot()}/>
</View>
<View style={styles.change}>
<Button title="change" onPress={() => setChange(!change)}/>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center' },
fire:{position:"absolute", width:"100%", bottom:0},
change:{position:"absolute", width:"100%", bottom:48}
});
function SettingsScreen() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding:8 }}>
<Text>{"Go to Home tab again, and notice the animation.\n\nEXCEPT if we changed the text... WHY?\n\nBUT still works if we fire the animation with the button, but after still won't work on focus detection... HOW?\n\nWorks if you hot reload / hard reload the app... HELP?"}</Text>
</View>
);
}
const Tab = createBottomTabNavigator();
function MyTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator>
);
}
export default function App() {
return (
<NavigationContainer>
<MyTabs />
</NavigationContainer>
);
}
It doesn't work because you're not following the rules of hooks. The following things are wrong in your code:
You're using variables from outside in useEffect hook, but passing empty dependency array
The animated values need to be in a useState or useRef hook so that they aren't recreated every render
Then the animation just won't start, even though the callback from focus listener is called and executed (check logs)... WHY?
The problem is that the callback is recreated after state update on re-render, so the callback passed to the focus listener isn't the same as what's in render anymore. And since you also don't have your animated values in state/ref, new animated values are also created while the old focus listener is referring to the old values. Basically the log you see is from an old listener and not the new one.
You should use the official eslint plugin and ensure that you fix all the warnings/errors from it so that such problems are avoided.
To fix your code, do the following changes:
const [xPos] = React.useState(() => new Animated.Value(initialXPos));
const [opacity] = React.useState(() => new Animated.Value(0));
const [change, setChange] = useState(true)
useEffect(() => {
const unsubscribe = navigation.addListener("focus", comingFromBot);
return unsubscribe;
}, [navigation, comingFromBot]);
const comingFromBot = useCallback(() => {
xPos.setValue(initialXPos);
opacity.setValue(0);
Animated.parallel([
Animated.spring(xPos, {
toValue: 100,
tension:3,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
]).start();
console.log("Animation's Fired!");
}, [xPos, opacity]);
I basically added useCallback, fix the dependency arrays, and moved the animated values to useState hook.
I finally ended with this, thanks to #satya164: Snack
I also wish I read this in documentation before.
HomeScreen's code:
// HomeScreen.js
function HomeScreen({navigation}) {
const initialXPos = Dimensions.get("window").height * 0.5 ;
const xPos = useRef(new Animated.Value(initialXPos)).current
const opacity = useRef(new Animated.Value(0)).current
const [change, setChange] = useState(true)
useEffect(() => {
const unsubscribe = navigation.addListener("focus", comingFromBot);
return unsubscribe;
}, [navigation, comingFromBot]);
const comingFromBot = useCallback(() => {
xPos.setValue(initialXPos);
opacity.setValue(0);
Animated.parallel([
Animated.spring(xPos, {
toValue: 100,
tension:3,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
]).start();
console.log("Animation's Fired!");
}, [xPos, opacity, initialXPos ]);
return (
<SafeAreaView style={{flex:1}}>
<Animated.View style={[
styles.container,
{ transform: [{ translateY: xPos }] },
{ opacity: opacity },
]}>
<Text style={{fontSize:30}}>{change ? "Home!" : "TIMMY!"}</Text>
</Animated.View>
{/* debug */}
<View style={styles.fire}>
<Button title="fire" onPress={() => comingFromBot()}/>
</View>
<View style={styles.change}>
<Button title="change" onPress={() => setChange(!change)}/>
</View>
</SafeAreaView>
);
}

Application wide Modal in React Native

I'm currently using react native modal and it serves the purpose of showing modals.
My problem currently is that I want to show the modal application wide. For example when a push notification received I want to invoke the modal regardless of which screen user is in. The current design of the modals bind it to a single screen.
How can this be overcome?
first of all make a context of your modal
const BottomModal = React.createContext();
then provide your modal using reactcontext provider
export const BottomModalProvider = ({children}) => {
const panelRef = useRef();
const _show = useCallback((data, type) => {
panelRef.current.show();
}, []);
const _hide = useCallback(() => {
panelRef.current.hide();
}, []);
const value = useMemo(() => {
return {
_show,
_hide,
};
}, [_hide, _show]);
return (
<BottomPanelContext.Provider value={value}>
{children}
<BottomPanel fixed ref={panelRef} />
</BottomPanelContext.Provider>
);
};
here is code for bottom panel
function BottomPanel(props, ref) {
const {fixed} = props;
const [visible, setVisibility] = useState(false);
const _hide = () => {
!fixed && hideModal();
};
const hideModal = () => {
setVisibility(false);
};
useImperativeHandle(ref, () => ({
show: () => {
setVisibility(true);
},
hide: () => {
hideModal();
},
}));
return (
<Modal
// swipeDirection={["down"]}
hideModalContentWhileAnimating
isVisible={visible}
avoidKeyboard={true}
swipeThreshold={100}
onSwipeComplete={() => _hide()}
onBackButtonPress={() => _hide()}
useNativeDriver={true}
style={{
justifyContent: 'flex-end',
margin: 0,
}}>
<Container style={[{flex: 0.9}]}>
{!fixed ? (
<View style={{flexDirection: 'row', justifyContent: 'flex-end'}}>
<Button
style={{marginBottom: 10}}
color={'white'}
onPress={() => setVisibility(false)}>
OK
</Button>
</View>
) : null}
{props.renderContent && props.renderContent()}
</Container>
</Modal>
);
}
BottomPanel = forwardRef(BottomPanel);
export default BottomPanel;
then wrap your app using the provider
...
<BottomModalProvider>
<NavigationContainer screenProps={screenProps} theme={theme} />
</BottomModalProvider>
...
lastly how to show or hide modal
provide a custom hook
const useBottomPanel = props => {
return useContext(BottomPanelContext);
};
use it anywhere in app like
const {_show, _hide} = useBottomModal();
//....
openModal=()=> {
_show();
}
//...
If you are not using hooks or using class components
you can easily convert hooks with class context
https://reactjs.org/docs/context.html#reactcreatecontext
this way you can achieve only showing the modal from within components
another way is store the panel reference globally anywhere and use that reference to show hide from non-component files like redux or notification cases.