enzymejs / enzyme

JavaScript Testing utilities for React
https://enzymejs.github.io/enzyme/
MIT License
19.95k stars 2.01k forks source link

Can't update state of functional component after invoking onTextChange #2535

Closed calebmsword closed 3 years ago

calebmsword commented 3 years ago

EDIT: It is clear after solving this problem that the state of the component was, in fact, updating, but that I accidentally performed assertions on a ReactWrapper that I did not invoke any updates to.

Current behavior

I have a React Native functional component that renders two TextInput components. Each TextInput has an onChangeText prop whose listener calls the setter returned from the useState hook to change one of two variables. However, when trying to run unit tests on this component, invoking onChangeText never results in any state change.

The component:

Essentially, the component has two text boxes whose values determine the state (the name and id of a new "Client" object that the component allows the user to create). The component also renders a button that, when pressed, calls a handler that takes two arguments, which are the two state variables of the component. A realistic application of this component would make an API call to create a new Client in the Client database.

import React, {useState} from 'react'
import {View, Text, TextInput, TouchableOpacity} from 'react-native';

const newClient = (newClientID:string, newClientName:string) => {
    console.log(newClientID+newClientName);
}

const ExampleComponent=( {addNewClient = newClient} ) => {
    const [newClientID, setNewClientID] = useState("initID")
    const [newClientName, setNewClientName] = useState("initName")
    return (
        <View>
            <Text>Please Enter New Client Information:</Text>
            <View>
                <Text>New Client ID:  </Text>
                <TextInput 
                    keyboardType="default" 
                    placeholder="New Client ID"                
                    onChangeText={text => setNewClientID(text)}                 
                />

                <Text>New Client Name: </Text>
                <TextInput
                    keyboardType="default" 
                    placeholder="New Client Name"                 
                    defaultValue={""}
                    onChangeText={text => setNewClientName(text)}                    
                />   
            </View>
            <View>
                <TouchableOpacity >
                    <Text onPress={() => addNewClient(newClientID, newClientName)} >
                        Add a New Client
                    </Text>
                </TouchableOpacity>
            </View>
        </View>
    )
}

export default ExampleComponent;

The test:

import { mount } from 'enzyme';

import { TextInput, Text } from 'react-native'; 
import ExampleComponent from './ExampleComponent';

let wrapper;
let id_input, name_input, button;

const mockAddNewClient = jest.fn();

describe('Can\'t change state in mounted functional component after invoking onTextChange', () => {

    beforeEach( () => {
        wrapper = mount(<ExampleComponent addNewClient={mockAddNewClient}/>)
        id_input = /* see repo linked below for specific way I found this component */
        name_input =  /* see repo linked below */
        button = /* see repo linked below */
    })

    const id = 'yeet';
    const name = 'skeet';
    it('inputing text in boxes and pressing button should have desired side effects', () => {
        id_input.invoke('onChangeText')(id);
        name_input.invoke('onChangeText')(name);
        button.invoke('onPress')();
        expect(mockAddNewClient).toHaveBeenCalledWith(id, name);
    })
})

Result of test

image

Actual Component behavior

Typing two words in the text boxes and pressing the "button" logs the expected string to console. image

Expected behavior

Invoking onChangeText (or simulating changeText) on a TextInput rendered in a mounted functional React Native component should result in state change; i.e., in the test shown above, we expect mockAddNewClient to have been called with "yeet" and "skeet" instead of the initial values of the component state.

Your environment

The application was built with React Native in a project initialized with Expo in a managed workflow. My tests are run using git bash on Windows 10.

You can access a repository that demonstrates this issue here. Feel free to clone the project if you wish.

I am currently mounting the component. I have also tried using shallow but it does not work either. If I have to use a method other than invoke to make this work, that is fine--all I care about is being able to test state changes caused by the onTextChange handler in a functional React Native component. Any suggestions would be appreciated.

API

Version

library version
enzyme 3.11.0
jest 26.6.3
react 16.13.1
react-dom 16.13.1
react-test-renderer n/a
adapter (below)

Adapter

calebmsword commented 3 years ago

Welp, I found my mistake.

The issue was finding the button before the state changed (I assign button in the callback sent to beforeEach). If I simply query for the button after invoking the textChanges, then the mocked function is called with the new state:

    const id = 'id';
    const name = 'name';
    it('inputing text in boxes and pressing button should have desired side effects', () => {
        id_input.invoke('onChangeText')(id);
        name_input.invoke('onChangeText')(name);

        button = wrapper
            .find(Text)
            .findWhere( node => 
                   node.text().toLowerCase().includes('add')
                && node.text().toLowerCase().includes('client')
                && ( typeof node.prop('onPress') !== 'undefined' )
            )
            .last(); // finding button here instead of in beforeEach is the secret sauce

        button.invoke('onPress')();
        expect(mockAddNewClient).toHaveBeenCalledWith(id, name);    
    })

This change makes the test pass :) image