What am I doing wrong (testing Picker component in React Native app)?

32 Views Asked by At

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.

0

There are 0 best solutions below