I have an Checkbox component that is an <input type=checkbox> inside a <label>.
When I try to simulate an event of click in the label it does not trigger my component onChange function.
Here is my component code:
const Checkbox = ({ disabled, onChange, checked, label }: Props) => (
<label
className={styles.label}
disabled={disabled}
>
<input
type='checkbox'
className={styles.checkbox}
checked={checked}
disabled={disabled}
onChange={onChange}
/>
{label}
</label>
);
My test:
it('calls onChange when the checkbox label is clicked', () => {
const wrapper = mount(<Checkbox label='Click me' onChange={onChange} />);
const label = wrapper.find('label');
label.simulate('click');
expect(onChange).toBeCalled();
});
To my understanding the simulate click should trigger the component onChange function. As it works when a user click it and when we trigger an click event by javascript.
Simulating click only calls onClick - the purpose isn't to test React event wiring, or browser event handling.
There's no need to test that React will trigger a "change" event when a checkbox is clicked - all you need is to shallow-render Checkbox, and assert that the input's onChange attribute matches the prop you passed.
I see. I am pretty new at testing react components. Thank you.
For anyone that stumbles upon this issue I found a work around that worked for me:
myLabel.getDOMNode().dispatchEvent(new Event('click'));
the purpose isn't to test React event wiring, or browser event handling.
True, but fwiw it's also really nice to have tests that closely match the mental-model/flow of the user, i.e. react-testing-libraries principle of "The more your tests resemble the way your software is used, the more confidence they can give you.".
So, agreed that technically yes, as an implementation detail I know that "click on this radio" will really do a change prop, but it'd be nice if this still "just worked", especially for mount which AFAIU renders down to DOM elements anyway.
Admittedly, it's an engineer/text UX issue and not a core functionality bug, but we're trying to use an approach of "props are internal impl details, the test should only interact with / touch / click DOM elements" (at least for full mount-based tests), and so hit this bump.
Thanks @dmmulroy for the work around, we'll try that!
So there is no intention to support this right? I can see use cases where you want to check that the proper Ids were assigned or that the render order is correct. If you fail In any of those cases, clicking the label will not trigger the change event, making you catch actual bugs on your code, not react implementation.
For anyone that stumbles upon this issue I found a work around that worked for me:
myLabel.getDOMNode().dispatchEvent(new Event('click'));
In which scenario does this work? In my case it does not work. I'm using implicit binding between label and checkbox, are you doing the same @dmmulroy ?
I can't find the example for a checkbox, but I do have radio buttons with the same pattern. I'm also not sure what you mean by implicit binding, but here is the relevant jsx:
<ul>
{options.map(option => (
<li key={option}>
<input
id={option}
type="radio"
name="reason"
value={option}
checked={option === selectedOption}
onChange={() => setSelectedOption(option)}
/>
<label htmlFor={option}>{option}</label>
</li>
))}
</ul>
and here is the test:
it('should enable the "No response" radio button when the label is clicked', () => {
const wrapper = mountWithTheme(<AbandonRequestModal {...props} />);
const radioButtons = wrapper.find('input');
const noResponseRadioButton = radioButtons.at(0);
expect(noResponseRadioButton.getDOMNode().checked).toEqual(false);
const labels = wrapper.find('label');
expect(labels.length).toEqual(6);
const noResponseLabel = labels.at(0);
expect(noResponseLabel.length).toEqual(1);
noResponseLabel.getDOMNode().dispatchEvent(new Event('click'));
expect(noResponseRadioButton.getDOMNode().checked).toEqual(true);
});
(btw, you should put those inputs inside their labels - both for full a11y coverage and because it makes the click target for the radio button bigger, and helps UX)
. I'm also not sure what you mean by implicit binding, but here is the relevant jsx
I mean this:
https://www.w3.org/WAI/tutorials/forms/labels/#associating-labels-implicitly
When the label is the parent of the checkbox, there is no need (in fact it is discouraged) to use the for or htmlFor property. That is my question, if it works in that scenario, without using htmlFor.
Thanks for the test by the way.
@danielo515 there is in fact a need to do both, because there are devices in use that only support linking, and others that only support nesting. What the spec says is irrelevant.
@dmmulroy does your click handler setSelectedOption gets called? For me it doesn't but the state of the html input node does change so the test initially passed. But when I checked if the function actually gets called it never does.
@lawnchamp I have no way of testing whether it gets called or not - it's internal state using useState and not a prop
Most helpful comment
True, but fwiw it's also really nice to have tests that closely match the mental-model/flow of the user, i.e. react-testing-libraries principle of "The more your tests resemble the way your software is used, the more confidence they can give you.".
So, agreed that technically yes, as an implementation detail I know that "click on this radio" will really do a change prop, but it'd be nice if this still "just worked", especially for
mountwhich AFAIU renders down to DOM elements anyway.Admittedly, it's an engineer/text UX issue and not a core functionality bug, but we're trying to use an approach of "props are internal impl details, the test should only interact with / touch / click DOM elements" (at least for full
mount-based tests), and so hit this bump.Thanks @dmmulroy for the work around, we'll try that!