Unable to find an element with a testID - react-native

I'm building a React Native app. Within my GigsByDay component, there is a TouchableOpacity element which, when pressed, directs the user to a GigDetails screen. I'm trying to test this particular functionality using Jest and React Native Testing Library.
I've written the following test, but have received the error:
Unable to find an element with testID: gigs-today-card
The test is as follows:
describe("gigs by week component", () => {
let navigation;
beforeEach(() => {
navigation = { navigate: jest.fn() };
});
test("that when gig listing is pressed on it redirects user to Gig Details page", () => {
render(<GigsByDay navigation={navigation} />);
const gigCard = screen.getByTestId("gigs-today-card");
fireEvent.press(gigCard);
expect(navigation.navigate).toHaveBeenCalledWith("GigDetails");
});
});
The element it's testing is as follows:
<TouchableOpacity
testID="gigs-today-card"
style={styles.gigCard}
onPress={() =>
navigation.navigate('GigDetails', {
venue: item.venue,
gigName: item.gigName,
blurb: item.blurb,
isFree: item.isFree,
image: item.image,
genre: item.genre,
dateAndTime: {...item.dateAndTime},
tickets: item.tickets,
id:item.id
})
}>
<View style={styles.gigCard_items}>
<Image
style={styles.gigCard_items_img}
source={require('../assets/Icon_Gold_48x48.png')}
/>
<View>
<Text style={styles.gigCard_header}>{item.gigName}</Text>
<Text style={styles.gigCard_details}>{item.venue}</Text>
</View>
</View>
</TouchableOpacity>
I've tried fixing my test as follows, but to no success:
test("that when gig listing is pressed on it redirects user to Gig Details page", async () => {
render(<GigsByDay navigation={navigation} />);
await waitFor(() => {
expect(screen.getByTestId('gigs-today-card')).toBeTruthy()
})
const gigCard = screen.getByTestId("gigs-today-card");
fireEvent.press(gigCard);
expect(navigation.navigate).toHaveBeenCalledWith("GigDetails");
});
});
Any suggestions on how to fix this? I also tried assigning the testID to the view within the TouchableOpacity element.
For context, here's the whole GigsByDay component:
import { FC } from 'react';
import { FlatList,TouchableOpacity,StyleSheet,View,Image,Text } from 'react-native'
import { listProps } from '../routes/homeStack';
import { GigObject } from '../routes/homeStack';
type ListScreenNavigationProp = listProps['navigation']
interface Props {
gigsFromSelectedDate: GigObject[],
navigation: ListScreenNavigationProp
}
const GigsByDay:FC<Props> = ({ gigsFromSelectedDate, navigation }):JSX.Element => (
<FlatList
testID='gigs-today'
data={gigsFromSelectedDate}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<TouchableOpacity
testID="gigs-today-card"
style={styles.gigCard}
onPress={() =>
navigation.navigate('GigDetails', {
venue: item.venue,
gigName: item.gigName,
blurb: item.blurb,
isFree: item.isFree,
image: item.image,
genre: item.genre,
dateAndTime: {...item.dateAndTime},
tickets: item.tickets,
id:item.id
})
}>
<View style={styles.gigCard_items}>
<Image
style={styles.gigCard_items_img}
source={require('../assets/Icon_Gold_48x48.png')}
/>
<View>
<Text style={styles.gigCard_header}>{item.gigName}</Text>
<Text style={styles.gigCard_details}>{item.venue}</Text>
</View>
</View>
</TouchableOpacity>
)}
/>
)

Please pass the gigsFromSelectedDate prop with some mock array data so that the flat list would render its elements based on the array length. Currently, you are not passing it. Please check the code below.
test('that when gig listing is pressed on it redirects user to Gig Details page', () => {
const mockData = [{venue: 'some venue',
gigName: 'some gigName,
blurb: 'some blurb',
isFree: 'some isFree',
image: 'some image',
genre: 'some genre',
dateAndTime: {},
tickets: ['some ticket'],
id: 'some id'}]
const screen = render(<GigsByDay navigation={navigation} gigsFromSelectedDate={mockData} />);
const gigCard = screen.getByTestId('gigs-today-card');
fireEvent.press(gigCard);
expect(navigation.navigate).toHaveBeenCalledWith('GigDetails');
});

Have you installed below package?
please check your Package.json
"#testing-library/jest-native": "^5.4.1"
if not please install it
then import it where you have written test cases.
import '#testing-library/jest-native/extend-expect';
if it is already present in then try below properties of jest-native.
const gigCard = screen.queryByTestId("gigs-today-card");
const gigCard = screen.findByTestId("gigs-today-card");

Related

How to pass data from a FlatList to a BottomSheet in React Native?

I used BottomSheet from https://gorhom.github.io/react-native-bottom-sheet/
Say I have a FlatList that renders buttons, how can I make it that when a certain button is pressed, the BottomSheet will open and will contain whatever data I passed in it? Say, the title data.
Here's my code:
import { BottomSheetModal, BottomSheetModalProvider, BottomSheetScrollView } from '#gorhom/bottom-sheet';
import { useRef, useState, useCallback, useMemo } from "react"
const DATA = [
{
id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb28ba',
title: 'John Doe',
},
{
id: '3ac68afc-c605-48d3-a4f8-fbd91aa97f63',
title: 'John Smith',
},
{
id: '58694a0f-3da1-471f-bd96-145571e29d72',
title: 'John Lennon',
},
];
const Item = ({ title }) => (
<AppButton title={title} style={{ marginVertical: 10 }} />
);
const App = () => {
// ref
const bottomSheetModalRef = useRef(null);
// variables
const snapPoints = useMemo(() => ['25%', '50%'], []);
// callbacks
const handlePresentModalPress = useCallback(() => {
bottomSheetModalRef.current?.present();
}, []);
const handleSheetChanges = useCallback((index) => {
console.log('handleSheetChanges', index);
}, []);
return (
<View style={styles.container}>
<FlatList
data={DATA}
keyExtractor={item => item.id}
renderItem={({ item }) => <Item title={item.title} />}
/>
</View>
<BottomSheetModalProvider>
<BottomSheetModal
ref={bottomSheetModalRef}
index={0}
snapPoints={snapPoints}
enablePanDownToClose
onChange={handleSheetChanges}
>
<BottomSheetScrollView>
</BottomSheetScrollView>
</BottomSheetModal>
</BottomSheetModalProvider>
)
}
I only copied much of the code for the BottomSheet from the docs and I have no knowledge yet about usecallback and useref so if what I want to happen needs those two, kindly indicate it :)
If im not using a list I managed to do it but had to create many copies of bottom sheet and refs and i know that is not very efficient
Refer to the screenshot below

you need to specify name or key when calling navigate with an object as the argument

i'm having an messages screen and i need to navigate to a "single message" when tapping to the List item of messages but i get this error "you need to specify name or key when calling navigate with an object as the argument"
i have created the "single message" screen and added it as a <Stack.Screen/> also but i don't know what i'm doing wrong.
below is my code:
function MessagesScreen({navigation}) {
const [messages, setMessages] = useState([]);
const [refreshing, setRefreshing] = useState(false);
const loadMessages = async () => {
const response = await messagesApi.getMessages();
setMessages(response.data);
}
useEffect(() => {
loadMessages();
}, []);
const handleDelete = message => {
setMessages(messages.filter((m) => m.id !== message.id));
}
return (
<Screen>
<FlatList
data={messages}
keyExtractor={message => message.id.toString()}
renderItem={({ item }) =>
<ListItem
title={item.fromUserId}
subTitle={item.content}
image={item.image}
onPress={() => navigation.navigate(routes.MESSAGE_SINGLE, item)}
renderRightActions={() =>
<ListItemDeleteAction onPress={() => handleDelete(item)} />}
/>
}
ItemSeparatorComponent={ListItemSeparator}
refreshing={refreshing}
onRefresh={() => {
setMessages([
{
id: 1,
title: 'T1',
description: 'D1',
image: require('../assets/mosh.jpg')
},
])
//setMessages(loadMessages());
}}
/>
</Screen>
);
}
const styles = StyleSheet.create({
})
export default MessagesScreen;
when i'm logging the "onPress" event on the console like this:
onPress={() => console.log('message selected', item)}
heres what i get:
and below is the MessageSingle screen i created to render the message but i dont know how to do it.
function MessageSingle() {
return (
<Screen>
<View style={styles.container}>
<AppText>{"kjhkjhjk"}</AppText>
{/* <AppText>{getMessagesApi}</AppText> */}
</View>
</Screen>
);
}
const styles = StyleSheet.create({
container: {}
});
export default MessageSingle;
so i want to get the message from the list of the messages. maybe i dont have to create e separate screen? i'm a beginner on this
any help would be appreciated!
you need to first add your MessageSingle component to the navigation container. Just put it as one of the screens along your MessagesScreencomponent. Then you need to navigate to it using that name:
onPress={() => navigation.navigate('MessageSingle', {item})}
the above will navigate to the screen with name MessageSingle, and passing the object item as a param.
in order to access this in your MessageSingle component, you need to use the route props.
function MessageSingle({route}) {
console.log('item = ', route.params?.item); // this would be your item.
return (
<Screen>
<View style={styles.container}>
<AppText>{"kjhkjhjk"}</AppText>
{/* <AppText>{getMessagesApi}</AppText> */}
</View>
</Screen>
);
}

Passing Data to Screen from FlatList Component

I created a custom component called AchievementBlock which is as follows:
import React from 'react';
import { View, StyleSheet, Image, TouchableOpacity } from 'react-native';
const AchievementBlock = ({ onPress }) => {
return (
<TouchableOpacity onPress={onPress}>
<View style={styles.AchievmentBlockStyle}>
<Image source={require('../../assets/images/Placeholder_Achievement.png')} />
</View>
</TouchableOpacity>
)
}
And used that component as a FlatList in my UserProfileScreen with a the following data:
const tournaments = [
{ title: 'Tournament Title #1', placement: '2nd', desc: 'Description Here', logo: require('../../assets/images/TournamentLogo.png') },
{ title: 'Tournament Title #2', placement: '1st', desc: 'Description Here', logo: require('../../assets/images/TournamentLogo.png') },
{ title: 'Tournament Title #3', placement: '3rd', desc: 'Description Here', logo: require('../../assets/images/TournamentLogo.png') },
]
I then return as follows:
return (
<View style={styles.container}>
<FlatList
data={tournaments}
numColumns={3}
keyExtractor={(tournaments) => {tournaments.title}}
renderItem={({ item }) => {
return (
<AchievementBlock
title={item.title}
placement={item.placement}
onPress={() => navigation.navigate('Achievement')}/>
)
}}
/>
</View>
)
Now I'm stuck on pulling through each rendered FlatList item's data to a new screen when clicking on it.
The on press would be something like this:
onPress={() => navigation.navigate('Achievement', {item.title, item.placement, item.logo, item.desc})}
But that doesn't work and with me still being a beginner with React Native I can't seem to find the solution in pulling each items data through to that screen with the navigation.getParam() function as normal.
What am I doing wrong and how would I achieve this in the best practice?
You're on the right path as React Navigation v5 uses the function
navigation.navigate
However the way you pass the key/value of item in the params argument is incorrect.
Try deconstructing with the following
onPress={
() => navigation.navigate('Achievement', { ...item })
}
or pass in the keys explicitly if you intend to set the keys/values
onPress={
() => navigation.navigate('Achievement', {
title: item.title,
logo: item.logo,
placement: item.placement,
desc: item.desc
})
}
Passing data through navigation
You can pass data with navigation as follows
onPress={
() => navigation.navigate('Achievement', {
name: item.name,
logo: item.logo,
})
}
Access data in next screen
export const Achievement = ({route}) => {
const name = route.params.name;
const logo = route.params.logo;
}

React Navigation 5 headerRight button function called doesn't get updated states

In the following simplified example, a user updates the label state using the TextInput and then clicks the 'Save' button in the header. In the submit function, when the label state is requested it returns the original value '' rather than the updated value.
What changes need to be made to the navigation headerRight button to fix this issue?
Note: When the Save button is in the render view, everything works as expected, just not when it's in the header.
import React, {useState, useLayoutEffect} from 'react';
import { TouchableWithoutFeedback, View, Text, TextInput } from 'react-native';
export default function EditScreen({navigation}){
const [label, setLabel] = useState('');
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableWithoutFeedback onPress={submit}>
<Text>Save</Text>
</TouchableWithoutFeedback>
),
});
}, [navigation]);
const submit = () => {
//label doesn't return the updated state here
const data = {label: label}
fetch(....)
}
return(
<View>
<TextInput onChangeText={(text) => setLabel(text) } value={label} />
</View>
)
}
Label should be passed as a dependency for the useLayouteffect, Which will make the hook run on changes
React.useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableWithoutFeedback onPress={submit}>
<Text>Save</Text>
</TouchableWithoutFeedback>
),
});
}, [navigation,label]);
Guruparan's answer is correct for the question, although I wanted to make the solution more usable for screens with many TextInputs.
To achieve that, I added an additional state called saving, which is set to true when Done is clicked. This triggers the useEffect hook to be called and therefore the submit.
export default function EditScreen({navigation}){
const [label, setLabel] = useState('');
const [saving, setSaving] = useState(false);
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableWithoutFeedback onPress={() => setSaving(true)}>
<Text>Done</Text>
</TouchableWithoutFeedback>
),
});
}, [navigation]);
useEffect(() => {
// Check if saving to avoid calling submit on screen unmounting
if(saving){
submit()
}
}, [saving]);
const submit = () => {
const data = {label: label}
fetch(....)
}
return(
<View>
<TextInput onChangeText={(text) => setLabel(text) } value={label} />
</View>
)
}

Tabbed navigation react native react-navigation

I have an app working with tabbed navigation but it seems that I need to interact with the components in the tabs that aren't active when the app starts, before they'll display data.
I have 3 tabs in my app, a map that displays restaurants nearby, a list of different ingredients and also a list of additives.
All these data sets are being sourced from a server (salesforce) when the page is loaded that holds the tab nav -- the homescreen component. The only thing this component is doing is loading my three other components.
Now, when I click into the other tabs, the screen is blank until i scroll or click in the page somewhere and then the UI loads. I think this is due to the fact that the setState call has already run, but when the 1st component in the tab nav was visible to the user.
How can I fire a call to update the UI when someone clicks on the newly active tab? (i'm setting state still in the component, not using redux yet.. this will come with time!)..
component below:
import React, {Component} from 'react';
import {
View,
FlatList,
ActivityIndicator
} from 'react-native';
import {Icon, List, ListItem, SearchBar} from 'react-native-elements';
import {oauth, net} from 'react-native-force';
// todo - implement... import {StackNavigator} from 'react-navigation';
export default class Ingredients extends Component {
static navigationOptions = {
tabBarLabel: 'Ingredients',
title: 'Ingredients',
tabBarIcon: ({tintColor}) => (
<Icon
name='blur-linear'
color={tintColor}
/>
)
};
constructor(props) {
super(props);
this.state = {
ingredients: [],
refreshing: false
};
}
componentDidMount() {
oauth.getAuthCredentials(
() => this.fetchData(), // already logged in
() => {
oauth.authenticate(
() => this.fetchData(),
(error) => console.log('Failed to authenticate:' + error)
);
});
}
componentWillUnmount() {
this.setState({
ingredients: [],
refreshing: false
})
};
fetchData = () => {
this.setState({
refreshing: true
});
net.query('SELECT Id, Ingredient__c, CA_GF_Status_Code__c, CA_Ingredient_Notes__c FROM Ingredient__c ORDER BY Ingredient__c',
(response) => this.setState({
ingredients: response.records,
refreshing: false
})
);
};
renderHeader = () => {
//todo - make this actually search something
return <SearchBar placeholder="Type Here..." lightTheme round/>;
};
renderFooter = () => {
if (!this.state.loading) return null;
return (
<View
style={{
paddingVertical: 20,
borderTopWidth: 1,
borderColor: "#CED0CE"
}}
>
<ActivityIndicator animating size="large"/>
</View>
);
};
selectIcon = (statusCode) => {
switch (statusCode) {
case 0:
return <Icon type='font-awesome' name='close' color='#80A33F'/>;
case 1:
return <Icon type='font-awesome' name='check' color='#80A33F'/>;
case 2:
return <Icon type='font-awesome' name='asterisk' color='#80A33F'/>;
case 3:
return <Icon type='font-awesome' name='sign-out' color='#80A33F'/>;
case 4:
return <Icon type='font-awesome' name='question-circle' color='#80A33F'/>;
default:
return <Icon type='font-awesome' name='close' color='#80A33F'/>;
}
};
render() {
return (
<List>
<FlatList
data={this.state.ingredients}
renderItem={({item}) => (
<ListItem
title={item.Ingredient__c}
subtitle={item.CA_Ingredient_Notes__c}
chevronColor='#025077'
avatar={this.selectIcon(item.CA_GF_Status_Code__c)}
onPress={() => {window.alert('this is being pressed')}}
/>
)}
keyExtractor={(item, index) => index}
ListHeaderComponent={this.renderHeader}
ListFooterComponent={this.renderFooter}
refreshing={this.state.refreshing}
onRefresh={this.fetchData}
/>
</List>
);
}
}