Trouble on testing onChange property from ant design - reactjs

I’m having trouble in testing the behaviour of an ant design component.
I use Text component from ant design with ‘editable’ and ‘onChange’ properties for editing a comment. For saving the new content I have to click anywhere on the page or to press enter and the onChange function will be triggered. I tested manually and everything works fine.
In tests, I manage to edit the content of the comment, but when I simulate the enter pressing (or the clicking on any DOM element) the onChange function is not called.
What should I do in order to trigger the onChange function?
Here's my component:
<Text editable={{ onChange: editComment }}
id={"edit-comment-" + props.commentIndex}>
{props.body}
</Text>
Here's my test:
the test includes both methods of triggering the onChange function, but I did not use both of them at the same time
test('Edit comment request', async () => {
const fakeResponse = {
success: 1
};
jest.spyOn(global, "fetch").mockImplementation(() =>
Promise.resolve({
json: () => Promise.resolve(fakeResponse)
})
);
const editButton = container.querySelector("span[aria-label='edit']");
await fireEvent.click(editButton);
// Edit the comment content
fireEvent.change(screen.getByRole('textbox'), { target: { value: "edited comment" } });
// Save the comment content by pressing enter
await fireEvent.keyPress(screen.queryByText(/edited comment/i),
{ key: "Enter", code: "Enter", keyCode:13, charCode: 13 });
// Save the comment content by clicking on a random DOM element
await fireEvent.click(container.querySelector('.ant-comment-content-author'));
await wait(() => expect(global.fetch).toHaveBeenCalled());
});

Try to set _defaultValue property before dispatch the onChange event:
const textbox = screen.getByRole('textbox');
textbox.value = "edited comment";
textbox._valueTracker.setValue(null);
fireEvent.change(textbox, {bubbles: true});
To React the value is still unchanged. Check this issue: https://github.com/facebook/react/issues/11488

Related

How to trigger up/down number input spin buttons in Jest React testing

I'm attempting to test using #testing-library/react & #testing-library/jest-dom a number input component and I haven't figured out how to test triggering the up/down spin buttons of the input.
test("renders and updates NumberInput", () => {
let value = "5";
const setValue = (e) => value = e.value;
render(<NumberInput
label="Quantity"
name="quantity"
value={value}
onChange={setValue}
step={0.5}
min={0.05}
/>);
let element = screen.getByRole('spinbutton');
expect(element).toBeInTheDocument();
fireEvent.change(element, { target: { value: "10" }});
expect(value).toEqual("10");
});
How could I test triggering the up/down spin buttons?
While I couldn't figure out how to simulate clicking the buttons, I did figure out how to test the step and min props working with the userEvent.keyboard. This triggers the up/down events of an input which in my case is the same as clicking the up/down spin buttons.
userEvent.click(element).then(() => {
expect(element).toHaveFocus();
userEvent.keyboard('[ArrowDown]').then(() => {
expect(value).toEqual("9.5");
});
});

Clearing a Draft.js textarea in automated testing

In my react app I am using a component that is building on top of draft.js.
Unfortunately I can not interact with it as I would with a normal textarea when using react-testing.
I want to input text, clear that text and then input new text. With an empty draft.js editor it is easy enough:
const textarea = screen.getByRole('textbox');
const event = createEvent.paste(textarea, {
clipboardData: {
types: ['text/plain'],
getData: () => 'MyText',
},
});
fireEvent(textarea, event);
I now want to clear the input in order to paste a new input. If I just invoke a second paste event, it will paste the data before what is already in the textarea so:
const textarea = screen.getByRole('textbox');
const event = createEvent.paste(textarea, {
clipboardData: {
types: ['text/plain'],
getData: () => 'MyText',
},
});
fireEvent(textarea, event);
fireEvent(textarea, event);
will result in the content of the textbox being MyTextMyText.
I tried using the change event to clear the textbox:
const textarea = screen.getByRole('textbox');
const changeEvent = createEvent.change(textarea, {
target: {
value: ''
},
});
fireEvent(textarea, changeEvent);
resulting in: The given element does not have a value setter
So my question is: What is the correct way to clear a draft.js textarea between pasting new content into it?

Use toHaveBeenCalledWith to check EventTarget

I've built a custom Input React component (think wrapper) that renders an HTML input element. When a value is entered the change is passed to the parent component like this:
const handleChange = (event: SyntheticInputEvent<EventTarget>) => {
setCurrentValue(event.target.value);
props.onChange(event);
};
Everything works as expected but now I want to write a test for it:
it('Should render a Text Input', () => {
const onChange = jest.fn();
const { queryByTestId } = renderDom(
<Input type={InputTypesEnum.TEXT} name='car' onChange={onChange} />
);
const textInput = queryByTestId('text-input');
expect(textInput).toBeTruthy();
const event = fireEvent.change(textInput, { target: { value: 'Ford' }});
expect(onChange).toHaveBeenCalledTimes(1);
});
This works fine too except that I want to add a final expect using toHaveBeenCalledWith. I've tried several things but can't figure out how to do it. Any ideas?
Update: I've been reading this: https://reactjs.org/docs/events.html#event-pooling. It appears that if I change handleChange like this:
const handleChange = (event: SyntheticInputEvent<EventTarget>) => {
event.persist();
setCurrentValue(event.target.value);
props.onChange(event);
};
then the received object from onChange does change to include my test data. That said, I don't like the idea of altering an important feature of production code (in this case, event pooling) simply to accommodate a test.
You can do something like this with toHaveBeenCalledWith
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
target: expect.objectContaining({
value: "Ford"
})
})
);

onClick Jest / enzyme : Method “simulate” is meant to be run on 1 node. 0 found instead

I am not being able to test onClick for the following code below. I keep getting the following error :
Method “simulate” is meant to be run on 1 node. 0 found instead.
My component code looks like this:
constructor (props) {
super(props)
this.state = INITIAL_STATE;
}
handleQuickFilter = (type) => {
this.setState({
quickFilterObj: {...this.state.quickFilterObj, [type]: {...this.state.quickFilterObj[type], checked: !this.state.quickFilterObj[type].checked}}
}, () => {
let filter = this.buildFilter();
filter ?
linker.UniversalGrid('Counterparty.Loa.Enrollment', '', `{form_filter}=$filter=(${filter}),{grid_selectable}=1,{can_add}=1`)
:
linker.UniversalGrid('Counterparty.Loa.Enrollment', '', `{form_filter}='',{grid_selectable}=1,{can_add}=1`)
})
}
render () {
return(
<div className='enrollment-grid-wrapper'>
<div className='quick-filter-div'>
<button className={this.state.quickFilterObj['New'] && this.state.quickFilterObj['New'].checked ? 'checked quick-filter' : 'unchecked quick-filter'}
id = 'testnew'
onClick={() => {this.handleQuickFilter('New')}}
>
New {this.state.quickFilterObj['New'] && this.state.quickFilterObj['New'].count !== undefined ? `(${this.state.quickFilterObj['New'].count})` : null}
</button>
<button className={this.state.quickFilterObj['Sent'] && this.state.quickFilterObj['Sent'].checked ? 'checked quick-filter' : 'unchecked quick-filter'}
onClick={()=>{this.handleQuickFilter('Sent')}}
>
Sent {this.state.quickFilterObj['Sent'] && this.state.quickFilterObj['Sent'].count !== undefined ? `(${this.state.quickFilterObj['Sent'].count})` : null}
</button>
)
}
I have tried the following, using Jest / enzyme:
it("should HandlequickFilter with button click", () => {
wrapper.setProps({});
wrapper.setState({quickFilterObj:"test"});
wrapper.find('.quick-filter-div').at(0).simulate("click");
expect(wrapper.state().quickFilterObj.New.checked).toEqual(true);
});
How could I make the following test to pass?
Since you are using css modules, your actual rendered DOM will not contain a div with class quick-filter-div. Instead, the class will be something like quick-filter-div__012xyz. Because of this, wrapper.find('.quick-filter-div') will return 0 nodes.
You can verify this by putting the following line in your test just after the setState line:
expect(wrapper.find('.quick-filter-div').exists()).toEqual(false)
But, it looks like what you want to test is the click of the button inside of your div.quick-filter-div. Isn't that correct? After all, the onClick handler is a prop for the button rather than the div.
And, because your button has been given an id, you can do:
it("should HandlequickFilter with button click", () => {
wrapper.setProps({});
wrapper.setState({quickFilterObj:"test"});
const button = wrapper.find('button#testnew') // find button based on id selector
expect(button.exists()).toEqual(true) // make sure button is found
button.simulate("click"); // simulate click
expect(wrapper.state().quickFilterObj.New.checked).toEqual(true);
});
If you didn't give an id to your button, but you knew that the button click you wanted to test was the first button (rather than the second one), then you could try:
it("should HandlequickFilter with button click", () => {
wrapper.setProps({});
wrapper.setState({quickFilterObj:"test"});
const buttons = wrapper.find('button') // find all buttons
expect(buttons).toHaveLength(2) // make sure you found 2 buttons
const button = buttons.at(0) // get the first button
button.simulate("click"); // simulate click
expect(wrapper.state().quickFilterObj.New.checked).toEqual(true);
});
Must add a return new Promise in your jest.fn function
//mocking functions
getSelectedLOAs = jest.fn( () => {
return new Promise((resolve, reject) => {
resolve()
})
});
it("should test click event NEW method", () => {
wrapper.setProps({
});
wrapper.find('COMPONENT').setState({
quickFilterObj:{
New: {
checked: true
},
Sent: {
checked: true
},
}, } );
wrapper.update();
wrapper.find('#New-testclick').simulate("click");
wrapper.find('#Sent-testclick').simulate("click");

Show custom dialog setRouteLeaveHook or history.listenBefore react-router/redux?

I cant seem to figure out how to show a custom dialogue instead of using the normal window.confirm that routeWillLeave and history.listenBefore uses. Basically i have built a notification system and check if a form is dirty const { dispatch, history, dirty } = this.props;
if the form is dirty it means the user has unsaved changes. If they change route I would like to show my notification which will have two buttons STAY, IGNORE which can both take an onClick handler.
Ive spent a bit of time googling and havent come across any mention of how i might accomplish this using routeWillLeave. The closest thing i could find was to use history.listenBefore however there docs say that I need to do this.
let history = createHistory({
getUserConfirmation(message, callback) {
callback(window.confirm(message)) // The default behavior
}
})
But I am using browserHistory from react-router to initiate my store const history = syncHistoryWithStore(browserHistory, store);
How can I stop a route change after a link has been clicked, show a notification using my custom notification system and depending on which button is clicked either transition to the new route or stay?
Here is an example of how my notification system works and the direction ive headed in which obviously doesn't work because all this returns is a string to show in the window.confirm dialogue by default.
history.listenBefore((location) => {
if(this.props.dirty){
const acceptBtn = {
title: 'STAY',
handler: ignoreRouteChange() //This can be any function I want
}
const denyBtn = {
title: 'IGNORE',
handler: continueRouteChange() //This can be any function I want
}
return dispatch(addNotification({
message: 'You have unsaved changes!',
type: NOTIFICATION_TYPE_WARNING,
duration: 0,
canDismiss: false,
acceptBtn,
denyBtn
}));
return "Usaved changes!"
}
});
Thanks
Another solution that i have decided to use is to return false in the normal setRouterLeaveHook callback and then show my dialog and use the nextLocation passed to the callback to push the next route depending on button selection.
router.setRouteLeaveHook(route, this.routerWillLeave.bind(this));
routerWillLeave(nextLocation){
let { dirty, dispatch, resetForm } = this.props;
if (dirty) {
let dialog = {
id: Date.now(),
showTitle: true,
titleContent: 'Unsaved Changes',
titleIcon: 'fa fa-warning',
content: <span>You have <strong>unsaved</strong> changes! <strong>Discard</strong> them?</span>,
type: 'confirm',
handleCloseClick: (e) => {
e.preventDefault();
dispatch(closeDialog());
},
acceptBtn: {
title: 'Okay',
handler: (e) => {
e.preventDefault();
resetForm();
// Wait for call stack to unwind and then execute
// the following which will now have the updated values.
setTimeout(() => {
dispatch(push(nextLocation));
dispatch(closeDialog());
}, 0);
}
},
denyBtn: {
title: 'Deny',
handler: (e) => {
e.preventDefault();
dispatch(closeDialog());
}
}
}
dispatch(addDialogWindow(dialog));
dispatch(openDialog(false, (e) => dispatch(closeDialog()), false));
return false;
}
return true;
}

Resources