Basically, I'm trying to test a component that have a select.
When trying to test the component, the test fails by returning the default value instead of the changed value.
But when I take the HTML of the rendered component (from screen.debug()) it works.
The component:
export function SelectFile({
fileList,
handleChange,
selected,
}) {
return (
<select
className="bg-slate-600 rounded w-auto"
onChange={onChange}
value={selected}
>
<option value="">Select an option</option>
<TodayOptions />
<AllOptions />
</select>
);
function AllOptions() {
return (
<>
{Object.entries(groups).map(([key, value]) => {
return (
<optgroup key={key} label={key.toLocaleUpperCase()}>
{[...value].sort(sortByDateFromLogs).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</optgroup>
);
})}
</>
);
}
function TodayOptions() {
const todayFiles = Object.values(groups)
.map((group) => {
const today = new Date().toLocaleDateString().replace(/\//g, '-');
return group.filter((file) => file.includes(today));
})
.flat();
if (todayFiles.length === 0) {
return null;
}
return (
<optgroup label="Today">
{todayFiles.map((item) => (
<option key={item}>{item}</option>
))}
</optgroup>
);
}
}
The original test:
it('should change option', () => {
render(
<SelectFile
fileList={fileList}
handleChange={handleChange}
selected=""
/>,
);
const selectElement = screen.getByDisplayValue('Select an option');
const allOptions = screen.getAllByRole('option');
const optionSelected = fileList.adonis[1];
expect(selectElement).toHaveValue('');
act(() => {
userEvent.selectOptions(selectElement, optionSelected);
});
expect(handleChange).toHaveBeenCalledTimes(1);
expect(selectElement).toHaveValue(optionSelected); // returns "" (default value)
expect((allOptions[0] as HTMLOptionElement).selected).toBe(false);
expect((allOptions[1] as HTMLOptionElement).selected).toBe(true);
expect((allOptions[2] as HTMLOptionElement).selected).toBe(false);
expect((allOptions[3] as HTMLOptionElement).selected).toBe(false);
expect((allOptions[4] as HTMLOptionElement).selected).toBe(false);
});
And the modified test with the rendered html:
it('should change option', () => {
render(
<div>
<div className="flex mr-10">
<h3 className="text-lg font-bold mr-4">Select a file</h3>
<select className="bg-slate-600 rounded w-auto">
<option value="">Select an option</option>
<optgroup label="ADONIS">
<option value="adonis-03-02-2022.json">
adonis-03-02-2022.json
</option>
<option value="adonis-02-02-2022.json">
adonis-02-02-2022.json
</option>
</optgroup>
<optgroup label="ERRORS">
<option value="http_errors-03-03-2022.log">
http_errors-03-03-2022.log
</option>
<option value="http_errors-04-02-2022.log">
http_errors-04-02-2022.log
</option>
</optgroup>
</select>
</div>
</div>,
);
const selectElement = screen.getByDisplayValue('Select an option');
const allOptions = screen.getAllByRole('option');
const optionSelected = fileList.adonis[1];
expect(selectElement).toHaveValue('');
act(() => {
userEvent.selectOptions(selectElement, optionSelected);
});
expect(selectElement).toHaveValue(optionSelected); // this returns the optionSelected value
expect((allOptions[0] as HTMLOptionElement).selected).toBe(false);
expect((allOptions[1] as HTMLOptionElement).selected).toBe(true);
expect((allOptions[2] as HTMLOptionElement).selected).toBe(false);
expect((allOptions[3] as HTMLOptionElement).selected).toBe(false);
expect((allOptions[4] as HTMLOptionElement).selected).toBe(false);
});
Considering it works with the modified test, I can't make it why it doesn't on the original. I've considered it was due to the optgroup, but it doesn't seems the case, so now I'm at a loss as to why.
Edit: the final version of the test:
it('should change option', () => {
const mockHandleChange = handleChange.mockImplementation(
(cb) => (e) => cb(e.target.value),
);
render(
<SelectWrapper fileList={fileList} handleChange={mockHandleChange} />,
);
const selectElement = screen.getByDisplayValue('Select an option');
const optionSelected = fileList.adonis[1];
expect(selectElement).toHaveValue('');
act(() => {
userEvent.selectOptions(selectElement, optionSelected);
});
expect(handleChange).toHaveBeenCalledTimes(2); // 1 for cb wrapper, 1 for select
expect(selectElement).toHaveValue(optionSelected);
});
});
const SelectWrapper = ({ handleChange, fileList }) => {
const [selected, setSelected] = useState('');
const mockHandleChange = handleChange(setSelected);
return (
<SelectFile
fileList={fileList}
handleChange={mockHandleChange}
selected={selected}
/>
);
};
I've created a wrapper to make it like you would use in another component, wrapped the mock function and now it changes the value and you have access to the mock.
Since in your test you are rendering only the Select
(which is a controlled component : it receives from its parent the current value and a onChange
callback), with a fixed selected
props, you cannot expect the selected option to change when you trigger a change event on the select. You can only expect that the onChange
callback has been called (like you do).
For this kind of component, you need to test that the selected
props is respected (the selected option is the good one), and that the provided callback is called when the user chooses a new option (you ahve done this part).
You need to add a test with an existing option as selected
props (not empty string), then check that the selected
option is the right one. I suggest you use https://github.com/testing-library/jest-dom#tohavevalue from https://github.com/testing-library/jest-dom.