Setting value and clicking a submit button with react-testing-library - reactjs

I have a test that unfortunately reveals some serious misunderstanding on my part on how to test this React web application using react-testing-library. The test:
const setup = () => {
const utils = render(<UnderConstruction />)
const input = utils.getByLabelText('Email')
return {
input,
...utils,
}
}
test('It should set the email input', () => {
const { input } = setup();
const element = input as HTMLInputElement;
fireEvent.change(element, { target: { value: 'abc#def' } });
console.log(element.value);
expect(element.value).toBe('abc#def');
})
The simple component (uses marterial-ui) looks like (I have removed large portions of it for brevity):
<form noValidate autoComplete="off" onSubmit={handleSubmit}>
<TextField
id="email-notification"
name="email-notification"
label="Email"
value={email}
onInput={(e: React.FormEvent<EventTarget>) => {
let target = e.target as HTMLInputElement;
setEmail(target.value);
}}
placeholder="Email" />
<Button
type="submit"
variant="contained"
color="secondary"
>
Submit
</Button>
</form>
First the as cast is to keep TypeScript happy. Second is mainly around my use of fireEvent to simulate a user input. Any ideas on how I can test this functionality? Righ now the test aways fails as it is expecting abc#def and receiving ''.

Because you are using TextField component of MUI Component, so you can not get real input with getByLabelText. Instead of that, you should use getByPlaceholderText.
Try this:
const setup = () => {
const utils = render(<UnderConstruction />)
const input = utils.getByPlaceholderText('Email')
return {
input,
...utils,
}
}
test('It should set the email input', () => {
const { input } = setup();
const element = input as HTMLInputElement;
fireEvent.change(element, { target: { value: 'abc#def' } });
console.log(element.value);
expect(element.value).toBe('abc#def');
})

Using UI library like this, if getByLabelText or getByPlaceholderText don't work, I usually get the underlying input element this way:
// add a data-testid="myElement" on the TextField component
// in test:
fireEvent.change(getByTestId('myElement').querySelector('input'), { target: { value: 'abc#def' } });
This is not ideal though, getByLabelText or getByPlaceholderText should be used when possible.

Related

How could I write tests for uncovered lines in useEffect and dispatch?

I recently started to write a test, so I am not familiar with that.
When I run :
$ yarn test:coverage
I face this result:
I want to get list of Models through dispatch when my drop-down will change (formik.values.make), and also when user select models, get list of trims.
How could I write a test for these two lines:
useEffect(() => {
if (formik.values.make) {
dispatch(carModelsSuccess(formik.values.make));// <-- 59 line ***
}
if (formik.values.model) {
dispatch(carTrimsSuccess(formik.values.model));// <--- 62 line ***
}
}, [dispatch, formik.values.make, formik.values.model])
and this part :
<FormControl className={classes.formControl} disabled={!formik.values.model}>
<InputLabel id="trim">Trim</InputLabel>
<Select
data-testid="trim"
labelId="trim"
id="trim"
name="trim"
value={formik.values.trim}
onChange={formik.handleChange}
>
{carTrims?.map(({ name, value }) => (
<MenuItem value={value} key={value}>{name}</MenuItem>
))}
</Select>
</FormControl>
I work with Next.js and I want write tests with Jest and #testing-library/react. I also used material-UI for select option.
I searched a lot in google on how to write tests for this code, but I couldn't find anything.
I succeeded in writing the test :
adding
inputProps={{ id: "trimInput" }} to select tag like this :
<Select
inputProps={{
id: "trimInput"
}}
data-testid="trim"
labelId="trim"
id="trim"
name="trim"
value={formik.values.trim}
onChange={formik.handleChange}
>
{carTrims?.map(({ name, value }) => (
<MenuItem value={value} key={value}>{name}</MenuItem>
))}
and this is test:
describe('fetch form options', () => {
it('dispatches the actions', async () => {
const { container } = render(<Provider store={store}><FormSection /></Provider>);
store.dispatch = jest.fn();
await act(async () => {
let makeSelectInput = container.querySelector(
"#makeInput"
) as HTMLDivElement;
fireEvent.change(makeSelectInput, { target: { value: "bmw" } });
});
expect(store.dispatch).toBeCalledWith(carModelsSuccess('bmw'));
await act(async () => {
let modelSelectInput = container.querySelector(
"#modelInput"
) as HTMLDivElement;
fireEvent.change(modelSelectInput, { target: { value: "320i" } });
});
expect(store.dispatch).toBeCalledWith(carTrimsSuccess('320i'));
});
});
useEffect will be called on render itself, you just need to render with the corresponding props that are needed.
Imagining your code is in a component named TestComponent that's getting the formik as props, see the code below
const props = {
formik: {
values: {
make: 'Japan' // a truthy value,
model: 2010 // a truthy value
}
},
dispatch: Jest.fn()
}
const {getByTestId} = render(<TestComponent {...props} />);
This will get you coverage for those lines.

Form validation problem in reactjs with react-hook-form

I'm trying to make form validation with react-hook-form. It works fine exept one propblem: it doesn't check input type. I want user to input only URL address, but this thing validate it as a simpte text. Where did I make a mistake?
function EditAvatarPopup({ isOpen, onClose, onUpdateAvatar, submitButtonName }) {
const { register, formState: { errors }, handleSubmit } = useForm();
const [link, setLink] = useState('');
function handleInput(e) {
setLink(e.target.value)
}
function handleSubmitButton() {
console.log(link)
onUpdateAvatar({
avatar: link
});
}
return (
<PopupWithForm
name="change-avatar"
title="Update avatar"
submitName={ submitButtonName }
isOpen={ isOpen }
onClose={ onClose }
onSubmit={ handleSubmit(() => handleSubmitButton() ) }
>
<label htmlFor="userAvatar" className="form__form-field">
<input
id="userAvatar"
type="url"
{ ...register('userAvatar', {
required: "Enter URL link",
value: link
})
}
placeholder="url link"
className="form__input"
onChange={ handleInput }
/>
{ errors.userAvatar && (<span className="form__error">{ errors.userAvatar.message }</span>) }
</label>
</PopupWithForm>
);
}
It looks like type="url" doesn't work, but I can't figure out why
Here is a minimal working example: https://codesandbox.io/s/react-hook-form-js-forked-ml3zx?file=/src/App.js
I recommend to not overwrite onChange props, instead of using const [link, setLink] = useState(''); you can use const link = watch('userAvatar').
You can remove the value property, it's not necessary:
<input
id="userAvatar"
type="url"
{ ...register('userAvatar', {
required: "Enter URL link",
value: link //⬅️ Remove that line
})
}
placeholder="url link"
className="form__input"
onChange={ handleInput } //⬅️ Remove that line
/>
React Hook Form - Watch API

How to set focus a ref using React-Hook-Form

How do you implement set focus in an input using React-Hook-Form, this is what their FAQ's "How to share ref usage" code here https://www.react-hook-form.com/faqs/#Howtosharerefusage
import React, { useRef } from "react";
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit } = useForm();
const firstNameRef = useRef();
const onSubmit = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input name="firstName" ref={(e) => {
register(e)
firstNameRef.current = e // you can still assign to ref
}} />
<input name="lastName" ref={(e) => {
// register's first argument is ref, and second is validation rules
register(e, { required: true })
}} />
<button>Submit</button>
</form>
);
}
I tried set focusing the ref inside useEffect but it doesn't work:
useEffect(()=>{
firstNameRef.current.focus();
},[])
Neither does inside the input:
<input name="firstName" ref={(e) => {
register(e)
firstNameRef.current = e;
e.focus();
}} />
You can set the focus using the setFocus helper returned by the useForm hook (no need to use a custom ref):
const allMethods = useForm();
const { setFocus } = allMethods;
...
setFocus('inputName');
https://react-hook-form.com/api/useform/setFocus
Are you using Typescript?
If so, replace...
const firstNameRef = useRef();
With...
const firstNameRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (firstNameRef.current) {
register(firstNameRef.current)
firstNameRef.current.focus()
}
}, []);
<input name="firstName" ref={firstNameRef} />
Got this from : https://github.com/react-hook-form/react-hook-form/issues/230
If you using Version 7, you can check this link in the docs
https://www.react-hook-form.com/faqs/#Howtosharerefusage
I can't comment (I don't have enough reputation), but without setting a timeout, setFocus didn't work. After adding a simple timeout from Subham's answer, it worked like a charm!
PS: Even adding a timeout of 0: setTimeout(() => setFocus(fieldName), 0) works. Can anyone explain?
I think you can simply use ref={(el) => el.focus()} to set the focus. The catch here is to make sure no other element within your page is also setting focus right after that el.focus() call.
const {
register,
handleSubmit,
setFocus, // here
formState: { errors },
} = useForm<FormFields>({
resolver: yupResolver(LoginSchema),
mode: "onTouched",
});
useEffect(() => {
setFocus("email");
}, [setFocus]);

#testing-library/react test form onSubmit

In a simple scenario like so
function onSubmit() { e.preventDefault(); /* Some Submit Logic */ }
<form data-testid="form" onSubmit={(e) => onSubmit(e)}>
<button type="submit" data-testid="submit-button">Submit</button>
</form>
How do I make sure that the form gets submitted when the submit button is clicked?
const { queryByTestId } = render(<LoginForm/>);
const LoginForm = queryByTestId("form")
const SubmitButton = queryByTestId("submit-button")
fireEvent.click(SubmitButton)
???
How do I test if onSubmit() has been called or maybe form has been submitted?
Basically, here is what I "solved" it:
// LoginForm.js
export function LoginForm({ handleSubmit }) {
const [name, setName] = useState('');
function handleChange(e) {
setName(e.target.value)
}
return (
<form data-testid="form" onSubmit={() => handleSubmit({ name })}>
<input required data-testid="input" type="text" value={name} onChange={(e) => handleChange(e)}/>
<button type="submit" data-testid="submit-button">Submit</button>
</form>
)
}
export default function LoginPage() {
function handleSubmit(e) {
// submit stuff
}
return <LoginForm handleSubmit={(e) => handleSubmit(e)}/>
}
Now the test's file:
// LoginForm.test.js
import React from 'react';
import { render, fireEvent } from "#testing-library/react";
import LoginPage, { LoginForm } from "./LoginPage";
it("Form can be submited & input field is modifiable", () => {
const mockSubmit = jest.fn();
const { debug, queryByTestId } = render(<LoginForm handleSubmit={mockSubmit}/>);
fireEvent.change(queryByTestId("input"), { target: { value: 'Joe Doe' } }); // invoke handleChange
fireEvent.submit(queryByTestId("form"));
expect(mockSubmit).toHaveBeenCalled(); // Test if handleSubmit has been called
expect(mockSubmit.mock.calls).toEqual([[{name: 'Joe Doe'}]]); // Test if handleChange works
});
getByRole('form', { name: /formname/i })
RTL is meant to move away from using id's. An ideal solution is to name your form which does two things. It allow you to uniquely name it making it useful to screen readers and also gets the browser to assign it a role of form. Without the name, getByRole('form') will do nothing.
MDN: <form> element
Implicit ARIA role - form if the form has an accessible name,
otherwise no corresponding role
I'd suggest to use nock to intercept request sending from form and return mocked response after.
For example:
nock('https://foo.bar').post('/sign-up', formValues).reply(201);
But I would like to know a better solutions tho.

how to test react-select with react-testing-library

App.js
import React, { Component } from "react";
import Select from "react-select";
const SELECT_OPTIONS = ["FOO", "BAR"].map(e => {
return { value: e, label: e };
});
class App extends Component {
state = {
selected: SELECT_OPTIONS[0].value
};
handleSelectChange = e => {
this.setState({ selected: e.value });
};
render() {
const { selected } = this.state;
const value = { value: selected, label: selected };
return (
<div className="App">
<div data-testid="select">
<Select
multi={false}
value={value}
options={SELECT_OPTIONS}
onChange={this.handleSelectChange}
/>
</div>
<p data-testid="select-output">{selected}</p>
</div>
);
}
}
export default App;
App.test.js
import React from "react";
import {
render,
fireEvent,
cleanup,
waitForElement,
getByText
} from "react-testing-library";
import App from "./App";
afterEach(cleanup);
const setup = () => {
const utils = render(<App />);
const selectOutput = utils.getByTestId("select-output");
const selectInput = document.getElementById("react-select-2-input");
return { selectOutput, selectInput };
};
test("it can change selected item", async () => {
const { selectOutput, selectInput } = setup();
getByText(selectOutput, "FOO");
fireEvent.change(selectInput, { target: { value: "BAR" } });
await waitForElement(() => getByText(selectOutput, "BAR"));
});
This minimal example works as expected in the browser but the test fails. I think the onChange handler in is not invoked. How can I trigger the onChange callback in the test? What is the preferred way to find the element to fireEvent at? Thank you
In my project, I'm using react-testing-library and jest-dom.
I ran into same problem - after some investigation I found solution, based on thread: https://github.com/airbnb/enzyme/issues/400
Notice that the top-level function for render has to be async, as well as individual steps.
There is no need to use focus event in this case, and it will allow to select multiple values.
Also, there has to be async callback inside getSelectItem.
const DOWN_ARROW = { keyCode: 40 };
it('renders and values can be filled then submitted', async () => {
const {
asFragment,
getByLabelText,
getByText,
} = render(<MyComponent />);
( ... )
// the function
const getSelectItem = (getByLabelText, getByText) => async (selectLabel, itemText) => {
fireEvent.keyDown(getByLabelText(selectLabel), DOWN_ARROW);
await waitForElement(() => getByText(itemText));
fireEvent.click(getByText(itemText));
}
// usage
const selectItem = getSelectItem(getByLabelText, getByText);
await selectItem('Label', 'Option');
( ... )
}
This got to be the most asked question about RTL :D
The best strategy is to use jest.mock (or the equivalent in your testing framework) to mock the select and render an HTML select instead.
For more info on why this is the best approach, I wrote something that applies to this case too. The OP asked about a select in Material-UI but the idea is the same.
Original question and my answer:
Because you have no control over that UI. It's defined in a 3rd party module.
So, you have two options:
You can figure out what HTML the material library creates and then use container.querySelector to find its elements and interact with it. It takes a while but it should be possible. After you have done all of that you have to hope that at every new release they don't change the DOM structure too much or you might have to update all your tests.
The other option is to trust that Material-UI is going to make a component that works and that your users can use. Based on that trust you can simply replace that component in your tests for a simpler one.
Yes, option one tests what the user sees but option two is easier to maintain.
In my experience the second option is just fine but of course, your use-case might be different and you might have to test the actual component.
This is an example of how you could mock a select:
jest.mock("react-select", () => ({ options, value, onChange }) => {
function handleChange(event) {
const option = options.find(
option => option.value === event.currentTarget.value
);
onChange(option);
}
return (
<select data-testid="select" value={value} onChange={handleChange}>
{options.map(({ label, value }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
);
});
You can read more here.
Finally, there is a library that helps us with that: https://testing-library.com/docs/ecosystem-react-select-event. Works perfectly for both single select or select-multiple:
From #testing-library/react docs:
import React from 'react'
import Select from 'react-select'
import { render } from '#testing-library/react'
import selectEvent from 'react-select-event'
const { getByTestId, getByLabelText } = render(
<form data-testid="form">
<label htmlFor="food">Food</label>
<Select options={OPTIONS} name="food" inputId="food" isMulti />
</form>
)
expect(getByTestId('form')).toHaveFormValues({ food: '' }) // empty select
// select two values...
await selectEvent.select(getByLabelText('Food'), ['Strawberry', 'Mango'])
expect(getByTestId('form')).toHaveFormValues({ food: ['strawberry', 'mango'] })
// ...and add a third one
await selectEvent.select(getByLabelText('Food'), 'Chocolate')
expect(getByTestId('form')).toHaveFormValues({
food: ['strawberry', 'mango', 'chocolate'],
})
Thanks https://github.com/romgain/react-select-event for such an awesome package!
Similar to #momimomo's answer, I wrote a small helper to pick an option from react-select in TypeScript.
Helper file:
import { getByText, findByText, fireEvent } from '#testing-library/react';
const keyDownEvent = {
key: 'ArrowDown',
};
export async function selectOption(container: HTMLElement, optionText: string) {
const placeholder = getByText(container, 'Select...');
fireEvent.keyDown(placeholder, keyDownEvent);
await findByText(container, optionText);
fireEvent.click(getByText(container, optionText));
}
Usage:
export const MyComponent: React.FunctionComponent = () => {
return (
<div data-testid="day-selector">
<Select {...reactSelectOptions} />
</div>
);
};
it('can select an option', async () => {
const { getByTestId } = render(<MyComponent />);
// Open the react-select options then click on "Monday".
await selectOption(getByTestId('day-selector'), 'Monday');
});
An easy way to test is by doing what the user should do
Click on the select field.
Click on one of the items in the dropdown list.
function CustomSelect() {
const colourOptions = [
{ value: 'orange', label: 'Orange', color: '#FF8B00' },
{ value: 'yellow', label: 'Yellow', color: '#FFC400' }
]
return <Select
aria-label="my custom select"
options={colourOptions}
//... props
/>
}
import { act, render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
// another imports
test('show selected item...', async () => {
const { getByText, getByLabelText } = render(<CustomSelect />);
expect(getByText('Orange')).not.toBeInTheDocument();
const myCustomSelect = getByLabelText(/my custom select/i);
await act(async () => userEvent.click(myCustomSelect));
const selectedItem = getByText('Orange');
await act(async () => userEvent.click(selectedItem));
expect(getByText('Orange')).toBeInTheDocument();
});
This solution worked for me.
fireEvent.change(getByTestId("select-test-id"), { target: { value: "1" } });
Hope it might help strugglers.
In case you are not using a label element, the way to go with react-select-event is:
const select = screen.container.querySelector(
"input[name='select']"
);
selectEvent.select(select, "Value");
export async function selectOption(container: HTMLElement, optionText: string) {
let listControl: any = '';
await waitForElement(
() => (listControl = container.querySelector('.Select-control')),
);
fireEvent.mouseDown(listControl);
await wait();
const option = getByText(container, optionText);
fireEvent.mouseDown(option);
await wait();
}
NOTE:
container: container for select box ( eg: container = getByTestId('seclectTestId') )
An alternative solution which worked for my use case and requires no react-select mocking or separate library (thanks to #Steve Vaughan) found on the react-testing-library spectrum chat.
The downside to this is we have to use container.querySelector which RTL advises against in favour of its more resillient selectors.
if for whatever reason there is a label with the same name use this
const [firstLabel, secondLabel] = getAllByLabelText('State');
await act(async () => {
fireEvent.focus(firstLabel);
fireEvent.keyDown(firstLabel, {
key: 'ArrowDown',
keyCode: 40,
code: 40,
});
await waitFor(() => {
fireEvent.click(getByText('Alabama'));
});
fireEvent.focus(secondLabel);
fireEvent.keyDown(secondLabel, {
key: 'ArrowDown',
keyCode: 40,
code: 40,
});
await waitFor(() => {
fireEvent.click(getByText('Alaska'));
});
});
or If you have a way to query your section—for example with a data-testid—you could use within:
within(getByTestId('id-for-section-A')).getByLabelText('Days')
within(getByTestId('id-for-section-B')).getByLabelText('Days')
Because I wanted to test a component that wrapped react-select, mocking it with a regular <select> element wouldn't have worked. So I ended up using the same approach that the package's own tests use, which is supplying a className in props and then using it with querySelector() to access the rendered element in the test:
const BASIC_PROPS: BasicProps = {
className: 'react-select',
classNamePrefix: 'react-select',
// ...
};
let { container } = render(
<Select {...props} menuIsOpen escapeClearsValue isClearable />
);
fireEvent.keyDown(container.querySelector('.react-select')!, {
keyCode: 27,
key: 'Escape',
});
expect(
container.querySelector('.react-select__single-value')!.textContent
).toEqual('0');
To anyone out there - I got mine to select by doing fireEvent.mouseDown on the option instead of click.

Resources