I'm trying to test a react native (expo) app and have had success manipulating text inputs so far, however when I try to test the behavior of a custom select field (Picker), that triggers a state change in the component upon change in value, I can't do it because the rendered component sets the value to undefined. What am I doing wrong?
TESTS
import React from 'react';
import { render, fireEvent, act } from '@testing-library/react-native';
import BeneficiaryFormScreen from '../app/new-service/beneficiary';
import LoginScreen from '../app/login';
import '@react-navigation/native'
import { renderRouter, screen } from 'expo-router/testing-library';
jest.mock('expo-router')
function isDisabled(element: any): boolean {
return !!element?.props.onStartShouldSetResponder?.testOnly_pressabilityConfig()?.disabled;
}
describe("Beneficiary form screen", () => {
it("Should enable button upon filling service reason and beneficiary name fields", async () => {
const { findByTestId } = render(<BeneficiaryFormScreen />)
const submitButton = await findByTestId('submit-button')
const serviceReason = await findByTestId("service-reason");
const beneficiaryName = await findByTestId("beneficiary-name");
// Simulate selecting an option
await act(async () => {
fireEvent(serviceReason, 'onValueChange', 'Instalada')
fireEvent(beneficiaryName, 'onChangeText', 'Roberto Mello');
})
// Check if the button is disabled
expect(isDisabled(submitButton)).toEqual(false);
})
})
COMPONENT I'M TESTING
import { Alert, ScrollView, StyleSheet, Text, View } from "react-native";
import Button from "../../components/Button";
import Input from "../../components/Input";
import { useCallback, useMemo, useState } from "react";
import { useFocusEffect, useRouter } from "expo-router";
import { useServicesContext } from "../../contexts/ServiceContext";
import { Select } from "../../components/Select";
import { maintenanceReasonOptions, reasonsOptions, receptorSwapOptions } from "../../constants/options";
import { Textarea } from "../../components/Textarea";
import { useFormSchema } from "../../hooks/useFormSchemas";
export default function BeneficiaryFormScreen() {
const router = useRouter()
const { finishBeneficiaryForm, resetFormState, currentForm, currentService } = useServicesContext()
const { beneficiarySpecs } = useFormSchema()
const [name, setName] = useState<string>('')
const [reason, setReason] = useState<any>(beneficiarySpecs.serviceReason.options[0])
const [observation, setObservation] = useState<string>('')
const [receptorSwap, setReceptorSwap] = useState<any>(receptorSwapOptions[0])
const isReproved = useMemo(() => {
return currentService?.remoteStatus === 'REPROVED'
}, [currentService])
const disabled = useMemo(() => {
if (isReproved) {
return false
}
if (reason.value !== 'Instalada' || currentService?.type === 'Manutenção') {
return !name || !observation
}
return !name
}, [name, reason, observation])
const notInstalled = useMemo(() => {
return reason.value !== 'Instalada'
}, [reason])
const handleSubmit = async() => {
if (notInstalled && !observation) {
Alert.alert('Atenção', 'Preencha a observação para continuar.')
return
}
if (
currentService?.type === 'Manutenção' &&
reason.value === 'Executado - Procedente' &&
receptorSwap === receptorSwapOptions[0]
) {
Alert.alert('Atenção', 'Selecione uma opção referente à troca do receptor')
return
}
if (currentForm?.beneficiary) {
const form = JSON.parse(currentForm.beneficiary)?.data
if (form.reason.value !== reason.value && !isReproved) {
Alert.alert('Atenção.', 'Ao modificar o motivo do atendimento, você perderá os dados preenchidos anteriormente. Deseja continuar?', [
{
text: 'Cancelar',
style: 'cancel',
},
{
text: 'Continuar',
onPress: async() => {
try {
await resetFormState()
await finishBeneficiaryForm({
name,
reason,
observation,
receptorSwap
})
router.back()
} catch (error) {
Alert.alert('Erro', 'Ocorreu um erro ao tentar finalizar o formulário. Tente novamente.')
}
}
}
])
return
}
}
await finishBeneficiaryForm({
name,
reason,
observation,
receptorSwap
})
router.back()
}
useFocusEffect(useCallback(() => {
if (currentForm?.beneficiary) {
const data = JSON.parse(currentForm.beneficiary).data
setName(data.name)
setReason(data.reason)
setObservation(data.observation)
}
}, [currentForm]))
return (
<View style={styles.container}>
<ScrollView
style={{ flex: 1, backgroundColor: '#fff' }}
>
<View style={styles.itemsContainer}>
<Text style={styles.title}>
Preencha os dados do atendimento.
</Text>
<Text style={styles.subtitle}>
Os campos com o status "Obrigatório" são necessários para concluir o cadastro.
</Text>
</View>
<View style={{ paddingHorizontal: 16, paddingVertical: 32, backgroundColor: '#fff', flex: 1, height: '100%' }}>
<Select
options={beneficiarySpecs.serviceReason.options}
selectedValue={reason}
label="Motivo do atendimento"
required={beneficiarySpecs.serviceReason.required}
onValueChange={(item) => setReason(item)}
testID="service-reason"
/>
{beneficiarySpecs.beneficiaryName.visible && (
<Input
label="Nome do representante"
placeholder="Nome do representante"
required
onChange={text => setName(text)}
value={name}
testID="beneficiary-name"
/>
)}
<Textarea
label="Observação"
placeholder="Insira sua observação"
optional={!notInstalled || currentService?.type !== 'Manutenção'}
required={notInstalled || currentService?.type === 'Manutenção'}
onChange={text => setObservation(text)}
value={observation}
/>
{(currentService?.type === 'Manutenção' && reason.value === 'Executado - Procedente') && (
<Select
options={receptorSwapOptions}
selectedValue={receptorSwap}
label="Troca do receptor"
required
onValueChange={(item) => setReceptorSwap(item)}
/>
)}
</View>
</ScrollView>
<View style={styles.buttonsContainer}>
<Button
title="Finalizar formulário"
onPress={handleSubmit}
disabled={disabled}
testID="submit-button"
/>
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#f0f2f7',
width: '100%',
flex: 1,
},
title: {
fontSize: 20,
color: "#101010",
fontWeight: "500",
marginBottom: 6,
marginTop: 16,
},
subtitle: {
fontSize: 14,
color: "#202020",
fontWeight: "300",
marginBottom: 32,
},
itemsContainer: {
width: '100%',
paddingHorizontal: 16,
paddingTop: 24,
backgroundColor: '#f0f2f7',
},
buttonsContainer: {
backgroundColor: '#fff',
paddingHorizontal: 16,
paddingVertical: 10,
borderTopColor: '#dedfe0',
borderTopWidth: 2,
position: 'fixed',
bottom: 0,
},
});
CUSTOM SELECT COMPONENT
import { Picker as Element } from '@react-native-picker/picker';
import { StyleSheet, Text, View } from 'react-native';
export const Select = ({
options,
selectedValue,
onValueChange,
style,
label,
required,
optional,
testID,
...props
}: {
options: any[],
selectedValue: any,
onValueChange: (value: any) => void,
style?: any,
label?: string,
required?: boolean,
optional?: boolean,
testID?: string,
}) => {
return (
<View style={[styles.container, style]}>
<View style={{ display: 'flex', flexDirection: 'row', backgroundColor: 'transparent' }}>
<Text style={styles.label}>{label}</Text>
{required && (
<Text style={{ color: 'red', marginLeft: 5, fontSize: 14 }}>*</Text>
)}
{optional && (
<Text style={{ color: '#404040', marginLeft: 5, fontSize: 13 }}>(opcional)</Text>
)}
</View>
<View
style={{
borderWidth: 1,
borderColor: '#d5d5d5',
borderRadius: 4,
overflow: 'hidden',
marginBottom: 16,
}}
>
<Element
selectedValue={selectedValue.value}
onValueChange={(_, idx) => onValueChange(options[idx])}
style={{
height: 48,
width: '100%',
backgroundColor: '#fff',
paddingHorizontal: 16,
paddingVertical: 8,
}}
testID={testID}
{...props}
>
{options.map(item => (
<Element.Item
style={{
color: '#202020',
fontSize: 16,
}}
key={item.value}
label={item.label}
value={item.value}
/>
))}
</Element>
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
width: '100%',
marginBottom: 4,
backgroundColor: 'transparent',
},
label: {
fontSize: 13,
marginBottom: 5,
color: '#030712',
},
input: {
backgroundColor: 'transparent',
borderRadius: 5,
borderWidth: 1,
borderColor: '#ddd',
paddingVertical: 10,
paddingHorizontal: 16,
fontSize: 16,
}
})
I have tried a variety of other ways of simulating the event of changing the value of the Picker field, but it seems that it's being correctly triggered in the test case.