React Native Testing Library fireEvent.press not triggering onPress - javascript

I'm trying to get an integration test working in React Native (using Jest and RN Testing Library) and all my other tests work as expected but this one it seems the fireEvent.press() stops actually firing off the onPress event from the button TouchableHighlight for a specific component
Workflow:
List of people
press nav item to pull up filters (narrow down list)
inside modal 2 options of filter types, select "people" filters
renders new list of filters and select one of these
updates list of people and closes modal
user presses "You know it" button
console.log() or setPerson in onButtonPress never fires
View with button
export default function PondLeadSnippet({person, setPerson}) {
const onButtonPress = useCallback(() => {
console.log("========> onButtonPress");
setPerson({...person, flagged: true});
}, [false]);
return (
<View style={Style.container}>
<View style={Style.content}>
<Label accessibilityRole='person-name' scale='medium'>{person.name}</Label>
<Text style={Style.detail}>{detail}</Text>
<Text>{person.flagged ? 'Flagged' : 'Not Flagged'}</Text>
</View>
<TouchableHighlight testID={`flag-person-${person.uuid}`} onPress={onButtonPress}}>
<Text>You know it</Text>
</TouchableHighlight>
</View>
);
}
My Test
test.only('can flag a person from list', async () => {
const { getByText, getAllByRole, findByTestId, getByTestId, store } = setup();
const bobSaget = people[1];
// Modal to view filters -> this works!
const filter = await findByTestId('people-filters');
await fireEvent.press(filter);
// toggle between 2 lists of filters -> this works!
await waitFor(() => expect(getByTestId("people-list")).toBeTruthy());
fireEvent.press(getByTestId("people-list"));
// select filter -> this works!
const peopleFilter = await findByTestId(`list-item-${someID}`);
fireEvent.press(peopleFilter);
// wait for list of people to update and modal closed
await waitFor(() => expect(getAllByRole('person-name')).toHaveLength(2));
const [ first, second ] = getAllByRole('person-name');
expect(first.children).toContain("Bob S.")
// press "Flag Person" button on person -> Does not work
expect(getByTestId(`flag-person-${bobSaget.uuid}`)).toBeTruthy();
fireEvent.press(getByTestId(`flag-person-${bobSaget.uuid}`));
// after event rerender should happen w/ new text element w/ "Flagged"
await waitFor(() => expect(getByText("Flagged")).toBeTruthy());
});

Your TouchableHighlight might have not been rendered when you try fire event. Try to replace
expect(getByTestId(`flag-person-${bobSaget.uuid}`)).toBeTruthy();
with
await waitFor(() => {
expect(getByTestId(`flag-person-${bobSaget.uuid}`)).toBeTruthy();
})

Using the test library, I realized the following.
When rendering the component with an onPress event, the render converts it to an onClick. According to the logic of the library it will never work. To solve it I did the following.
import {
fireEvent,
render
} from '#testing-library/react-native';
const element = render(
<Presseable onPress={()=>console.log('press')}>
<Text>I'm pressable!</Text>
</Pressable>
);
const { getByTestId } = element;
const button= getByTestId('pressable');
fireEvent(button, 'click'); // that's how it works
fireEvent(button, 'press'); // this is the equivalent to fireEvent.press(button) It does not work like this

Related

How to wait for user action in separate Modal before continuing function execution?

I'm developing a drink counter app. User can add drinks but also has a drinkLimit state that is used to remind the user if the limit is reached when adding drinks. This reminder pops up in a modal with cancel and continue buttons when drink count has reached it's limit. The user can then either cancel the new drink addition or continue with it.
The problem is that I need to somehow wait or check for the user's action (clicking either "cancel" or "continue" in the modal), and based on that either return out of the addDrink function or continue the addition. I'm not sure how to implement that.
Using async/await seems intuitive but how can I await for a function when that function runs only after user clicks a button?
Using callbacks might be another solution but there as well I would need to halt the execution until user makes his action.
This seems like a common use case but I still can't seem to find any questions or tuts about this specific case.
App.tsx
const App = () => {
const [drinklist, setDrinkList] = useState<DrinkType[]>();
const [favorites, setFavorites] = useState<FavDrinkType[] | null>(null);
const [drinkLimit, setDrinkLimit] = useState<number>();
const [actionCalled, setActionCalled] = useState<string | null>();
const addDrink = (alcPercent: number, amount: number, name?: string) => {
// if drink limit has been reached open reminder modal
if (drinkLimit && drinklist) {
if (drinklist?.length >= drinkLimit) {
// how do I pause the execution here?
const action = await openReminder();
//cancel add
if (action === 'cancel') {
setShowReminder(false);
return
}
}
}
// continue add
setShowReminder(false);
// *addition logic* (irrelevant)
};
// Opens the modal (and waits for the user's action?)
const openReminder = async () => {
setShowReminder(true);
// should I wait for function handleActionCalled()?
}
// Modal calls this and sets state
const handleActionCalled = async (action: string) => {
setActionCalled(action);
}
return (
<View>
<ReminderModal
showModal={showReminder}
handleActionCalled={handleActionCalled}
/>
</View>
ReminderModal.tsx:
const ReminderModal = ({
showModal,
handleActionCalled,
}: ReminderModalProps) => {
const handleAction = (action: string) => {
switch (action) {
case 'cancel':
return handleActionCalled('cancel');
case 'continue':
return handleActionCalled('continue');
}
};
return (
<Modal visible={showModal} animationType="slide" transparent={true}>
<TouchableOpacity onPress={() => handleAction('cancel')}>
<View>
<Text>Cancel</Text>
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleAction('continue')}>
<View>
<Text>Continue</Text>
</View>
</TouchableOpacity>
</Modal>
Seems you are overcomplicating the issue, you should break down your flow in multiple steps:
On first action => open modal if the addDrinks estimate that it reaches the limit. Should be the end of this function
a/ if hit cancel => trigger a
function that closes the modal
b/if hit continue trigger the
addDrinks function
I don't think you should include the opening of the modal as part of your addDrink function, or it should trigger a new flow but not await for an answer here.

React Native To-Do app does not update state on click but does after another state updates

I tried making a to-do app using React-native. It is a blank react native project that uses typescript and expo. I'm on linux.
The state containing the todo array gets updated it should but the view is not rendered accordingly untill I click the input button and change the text state.
I have 3 functions to modify the states - textChange (for updating the todoText state), handleAddTask (for updating the array containing tasks), rmTask ( removes item from task array)
const [task, setTask] = useState('');
const [taskItems, setTaskItems] = useState(["Write a task"]);
const textChange = (text: string) => {
setTask(text)
}
const handleAddTask = () => {
taskItems.push(task)
setTaskItems(taskItems)
setTask('')
}
const rmTask = (index: number) => {
taskItems.splice(index, 1);
setTaskItems(taskItems)
}
And the components where the functions are affecting
taskItems.map((item, index) => {
return (
<TouchableOpacity
key={index}
onPress={()=> rmTask(index)}
>
<Task text={item} />
</TouchableOpacity>)
})
const handleAddTask = () => {
// taskItems.push(task) <-- Dont mutate state like this !
setTaskItems([...taskItems, task]). <-- You should use it like this way.
setTask('')
}
You are mutating the state value without using the setter function. Thats why Its not working correctly. And when you write something new to task input, UI gonna re-render and display the correct thing. Because textChange function implementation correct and update the state.
In two places you are directly manipulating the state rather than using setState.
You should do this way:
//taskItems.splice(index, 1);
//setTaskItems(taskItems)
// -should be
setTaskItems([...taskItems].splice(index, 1))
and
//taskItems.push(task)
//setTaskItems(taskItems)
// -should be
setTaskItems([...taskItems, task])

How can I call a function from one component when a button in another component is clicked

Hi Guys I am a newbie in React-native I will like to know How I can called a function of one component when a button in another component is click like below I will like to call the onPlayPress() function from PlayWidget component when the button in AlbumHeader get click on
AlbumHeader.tsx file
export type AlbumHeaderProp = {
album: Album;
}
const AlbumHeader = (props: AlbumHeaderProp) => {
const { album } = props;
const playallSong = () => {
}
return (
<View style={styles.container}>
<TouchableOpacity onPress={playallSong}>
<View style={styles.button}>
<Text style={styles.buttonText} >
<PlayerWidget onPress={playallSong}/>
{Play}
</Text>
</View>
</TouchableOpacity>
</View>
)
}
export default AlbumHeader;
PlayWidget.tsx files
const onPlayPausePress = async () => {
if (!sound) {
return;
}
if (isPlaying) {
await sound.pauseAsync();
} else {
await sound.playAsync();
}
}
You could pass the function as prop to the component with the button. Then when you click the button, you can call that function
Component A - has function
Component B - has button
Assuming Component B is a child of Component A - then <Component B func={compAFunc}/>
Then in Component B button's onClick method
<btn onClick={props.func} />
If B is not a child of A, then you will probably have to lift state up to another component. Otherwise add the function to a global store like redux so it can be shared all over.
This is a good example of a use case for an observer pattern.
In the observer pattern there is a particular event that an "observer" cares about in the "publisher" (There are other names used for these things).
To sum it up, the "observer" (sometimes called "Subscriber") is the one who needs to be called when an event occurs. The "Publisher" or "Observable" is the one who will do the calling of all subscribers when a particular event occurs.
The "publisher" will have an "on" or "subscribe" function that registers the observer to be called for the particular event.
Here is a great post for more about the Observer Pattern
Refactoring Guru - Observer
How does it help you?
your code will look something like the following...
interface AlbumHeaderProps {
// ...
onPlayerWidgetPress(): Promise<void> // analogous to .subscribe(Observer)
}
const AlbumHeader = ({
onPlayerWidgetPress,
}: AlbumHeaderProps) => {
return (
<View style={styles.container}>
// ...
<PlayerWidget
onPress={onPlayerWidgetPress}
/>
// ...
</View>
)
}
Then in the code that calls this component...
<AlbumHeader
onPlayerWidgetPress={async () => { // analogous to .update() method on observer
//... your function
}}
/>
NOTE: I don't know how or if PlayerWidget honors/awaits promises so I can't speak to concurrency control here.
Your final solution may vary, but hopefully this helps to get you part of the way there.

How to make a button visible after function runs. React Native

I'm working a app that will check and API to see if there are active users, if there are active users I want to show a button a tt he bottom of the page
I'm calling the function 2 seconds after the page load, because if I don't do this I get an error
"Too mnany re-renders. React limist the number of.."
and the only
for me to fix this was to do the timeout.
After two second the function is called and setActive to true, but my button doesn't appears.
Yes I'm new to react native :/
export default function Index({navigation}) {
const [active, setActive] = useState(true);
const checkForActive = () => {
console.log("Actives");
setActive(true);
}
setTimeout(() => {
checkForActive();
}, 2000);
return (
<View style={styles.MainContainer}>
{!active && (
<TouchableOpacity activeOpacity={.45} style={styles.BNutton} >
<Text style={styles.Text}> Active</Text>
</TouchableOpacity>
)
}
</View>
);
}
You should use an effect for this:
As checkForActive calls an API, it's asynchronous, so I'm assuming it will eventually return a Promise
useEffect(() => {
checkForActive.then(data => {
// Do things with data
setActive(true);
});
}, []);
The effect will be triggered once on first rendering of the component, then the empty dependencies array as second parameter means that nothing will trigger the effect again during the component life time.

React native setState hook reopens modal

I have a weird issue with a modal in react native (expo). My modal looks like this:
const useNewCommentModal = () => {
const [showModal, setShowModal] = useState(false);
const [comment, setComment] = useState('');
const toggle = () => {
setShowModal(!showModal);
};
const NewCommentModal = () => (
<Modal visible={showModal} animationType="slide">
<View style={[t.pX4]}>
<TextInput
style={[t.bgWhite, t.p2, t.rounded, t.textLg]}
placeholder={'Post jouw reactie...'}
onChangeText={text => setComment(text)}
value={comment}
/>
</View>
</Modal>
);
return [toggle, NewCommentModal];
};
export default useNewCommentModal;
When I type the modal keeps reopening. When I remove this:
onChangeText={text => setComment(text)}
The problem goes away but obviously the state isn't updated anymore. How
come the model keeps reopening?
--EDIT--
const [toggleModal, NewCommentModal] = useNewCommentModal(
'user',
route.params.user.id
);
<NewCommentModal />
Every time your useNewCommentModal hook runs, it creates a new function called NewCommentModal which you then use as component <NewCommentModal /> (this part is very important). To React, every new NewCommentModal is different from a previous one (because you create new function each time and every render), React would run equality comparison between old and new NewCommentModal which will return false, so React will unmount old modal and mount a new one in it's place. That happens because you call function as component. So you should return not NewCommentModal component from your hook but a function that will render something instead (renderNewCommentModal) and call it not as a component but as a function ({renderNewCommentModal()}) instead. Or better yet, return only data from a hook, and use this data to render something in your main component
See my previous answer on this matter for more detailed explanation https://stackoverflow.com/a/60048240/8390411

Categories

Resources