How to test TextInput in react-native with #testing-library/reat-native? - javascript

I'm trying to test a TextInput by checking its value after entering a string, but it's not working. Any help would be appreciated.
Here's the code:
import React from 'react'
import { TextInput } from 'react-native'
import { fireEvent, render } from "#testing-library/react-native"
test('<TextInput/>, () => {
const { getByTestId } = render( <TextInput testID="input" /> );
const input = getByTestId("input");
fireEvent.changeText(input, "123");
expect(input.value).toBe("123");
})
The test fails with the message:
Expected: "123"
Received: undefined

I think your example is not working because you are not controlling the input. Try adding a value and an onChangeText function. Something like:
function Example() {
const [value, setValue] = useState('')
return <TextInput value={value} onChangeText={setValue} testID="input" />
}
test('Should apply the value when changing text', () => {
const { getByTestId } = render(<Exampler />);
const input = getByTestId('input');
fireEvent.changeText(input, '123');
expect(input.props.value).toBe('123');
});
Also, you need to check input.props.value instead of input.value
Hope it helps.

I'm going to suggest a few things to assist you to get to the solution,
Are you sure TextInput has a prop called testID? You use getByTestId subsequently, which needs a data-testid value on the element. So please make sure of this first.
You can try finding the element in another way. Probably use getByPlaceholderText or similar.
Once you can get the element properly, and after the fireEvent, the value should certainly be updated, and assertion would succeed.

Related

Function called inside a component doesn't have updated props

I have created a reproducible exam of my problem, I don't understand why after the setDefaultValue is called and the component is updated (you can see it's updated using the result of my console.log) If now I click on the reset button instead of the new defaultValue I see the old one.
Here is a link to the example showing this problem, I'll also paste the code here
https://codesandbox.io/s/wonderful-tree-wtsgb4?file=/src/App.js
import "./styles.css";
import {useState, useRef} from 'react';
import TextBox from './TextBox';
export default function App() {
const textboxAPI = useRef(null)
const [defaultValue ,setDefaultValue] = useState('First')
return (
<div className="App">
<div style={{fontWeight: 'bold'}}>To reproduce please first click on the default value button then on the reset button</div>
<TextBox getAPI={(api) => textboxAPI.current = api} defaultValue={defaultValue}/>
<button onClick={() => setDefaultValue('second')}>1- Click me to change default value to "second"</button>
<button onClick={() => textboxAPI.current.reset()}>2- Click me to call reset inside Textbox</button>
</div>
);
}
import {useEffect, useState} from 'react';
const TextBox = ({defaultValue, getAPI}) => {
const [value, setValue] = useState(defaultValue || '')
useEffect(() => {
if (getAPI) {
getAPI({
reset: reset,
})
}
}, [])
const reset = () => {
console.log('TextBox Reset DefaultValue', defaultValue)
setValue(defaultValue)
}
console.log('TextBox DefaultValue', defaultValue)
return <div>{value}</div>
}
export default TextBox;
To reproduce the problem:
1- Click on the first button to set a new defaultValue, see the console.log, you can see the defaultValue has changed inside the TextBox Component
2- Click the reset button, it calls the reset function inside TextBox but the default value logged there has the previous value!
Here you save in textboxAPI.current function reset but just one time after first render of TextBox component. Function reset has a defaultValue in a closure and its value is 'First' during first render. So each next time you call textboxAPI.current.reset(), you call the reset function with defaultValue==='First' in its closure.
But you parent component controls child state and React does not recommend to manage your logic like that.
[UPDATED]
That will fix your issue, but I don not recommend to organize a state logic like that:
const TextBox = ({defaultValue, getAPI}) => {
const [value, setValue] = useState(defaultValue || '')
const reset = () => {
console.log('TextBox Reset DefaultValue', defaultValue)
setValue(defaultValue)
}
if (getAPI) {
getAPI({
reset: reset,
})
}
console.log('TextBox DefaultValue', defaultValue)
return <div>{value}</div>
}
export default TextBox;
Based on what I learned from the comments I tried using Hooks but There were too many changes needed especially I had some issues with React.lazy so I tried to look for more solutions until I found that using a combination of forwardRef and useImperativeHandle I can export my reset function without changing my current structure and it works as it should, I thought that I should share my solution for anyone else who might be looking for an alternative solution to Hooks.

How to mock the elements of react-hook-form when testing with react-testing-library?

Having a basic component which uses react-hook-form:
const { handleSubmit, reset, control } = useForm({
resolver: yupResolver(schema)
});
...
<MyComponent
title='title'
open={isOpened}
control={control}
/>
This component has 3 props, title - a string, open - a function, control - no idea what is it, all of them mandatory.
So, when writing a test for it I got stuck here:
import { render } from '#testing-library/react';
import '#testing-library/jest-dom';
import MyComponent from './my-component';
describe('MyComponent', () => {
const title = 'title';
it('should render successfully', () => {
const { baseElement, getByText } = render(
<TaskModal
title={title}
open={jest.fn()}
control={} // what should be written here?
/>
);
expect(baseElement).toBeTruthy();
expect(getByText(title)).toBeTruthy();
});
How can control be mocked for this test?
Maybe just passing in the real control from useForm, like they are doing in react-hook-form's testing. https://github.com/react-hook-form/react-hook-form/blob/master/src/__tests__/useController.test.tsx
control came from useForm:
const { control } = useForm();
Control is necessary when you use Controller or useController
The doc: https://react-hook-form.com/api/usecontroller
I suppose the TaskModal component is in a form. I recommend to put the form inside the modal and it would be easier to test otherwise you can wrap your component (TaskModal) with a form for your test.
If anyone is getting any error while using hooks inside tests, try renderHook from testing-library/react:
import { render, renderHook } from '#testing-library/react'
const { result } = renderHook(() => useForm())
render(
<TextField control={result.current.control} />
)

React Native putting different User Inputs in one Object

I am trying to put together a simple Question Answer Quiz with React Native. That said I want to store both the question and the answer in one object.
Currently I am running into trouble with the following codepieces:
import React, { useState, useEffect } from 'react'
import {
StyleSheet,
Button,
View,
Text,
TextInput,
TouchableOpacity
} from 'react-native'
const Test = () => {
const[card, setValues] = useState({
title: '',
question: '',
answer: ''
})
const updateField = e => {
setValues({
...card,
[e.target.name]: e.target.value
});
};
<TextInput
placeholder='Kartentitel eingeben..'
name = "title"
value={card.title}
onChangeText={updateField}>
</TextInput>
<TextInput
placeholder='Fragestellung eingeben..'
name = "question"
value={card.question}
onChangeText={updateField}>
</TextInput>
<TextInput
placeholder='Antwortmöglichkeit eingeben..'
name = "answer"
value={card.answer}
onChangeText={updateField}>
</TextInput>
}
export default Test;
However I get the following Error
ERROR TypeError: undefined is not an object (evaluating 'e.target.value')
error
I've got the idea behind this from react(not react native) posts and tutorials.
Basically what this is trying to do is to get each corresponding UserInput value and put it into the object target(this is what the updateField function should do)
Any help would really appreciated
Richard
The error happens because e is not an HTMLInputChange event, I think that the onChangeText functions expects a functions with just a string argument.
You could wrapp the updateField function with another function that expects a name something like this:
const updateField = (name) => (value) =>{
setValues({
...card,
[name]: value
});
}
And use it like this
<TextInput ... onChangeText={updateField("title")} />
This should work, although I recommend using something like Formik to make form validations and handle the value changes.
Difference between onChangeText and onChange is that the argument on onChangeText is the actual value only whereas the argument on onChange is the event itself. So try replacing onChangeText with onChange.
<TextInput
placeholder='Antwortmöglichkeit eingeben..'
name = "answer"
value={card.answer}
onChange={updateField}>
</TextInput>

Can't figure out why React code doesn't work

I've built a custom Input component which is simply a wrapper for the HTML input element. Here's the critical code, which I've simplified for posting here:
// #flow
import React, { useState } from 'react';
type Props = {
value?: string,
onChange: Function
};
const Input = ((props: Props) => {
const [ currentValue, setCurrentValue ] = useState(!!props.value ? props.value : '');
const handleChange = (event: SyntheticInputEvent<EventTarget>) => {
setCurrentValue(event.target.value);
props.onChange(event);
};
return <input type='text'
value={currentValue}
onChange={handleChange} />;
});
I wrote a bunch of React Testing Library tests for this and they all pass fine. But when I implemented this component in a web page, the initial value failed to appear. I solved the problem by dropping the currentValue code and just using props.value instead. That solves it. But I'm most curious why this approach above fails to display the initial value.
Look at this code, I did use prop-types

enzyme jest find by displayName not working?

I have this very simple stateless component
const Text = () => 'text'
Text.displayName = 'MyText'
export default Text
My test case
import Text from './Components/Text'
import { shallow } from 'enzyme'
it('render Text', () => {
const wrapper = shallow(<Text />)
expect(wrapper.find('Text').length).toBe(1)
})
What's wrong? I also tried exists(), seems like this is not working wrapper.find('Text')
You are rendering <Text /> so wrapper will contain result of Text's rendering without that tag itself.
If you check wrapper.debug() you will see just "text".

Categories