I am trying to write a test for the following code. It uses MUI components to render a checkbox with label.
import React from 'react';
import { FormControlLabel, Checkbox } from '@mui/material';
function FilterCheckbox(props) {
return(
<FormControlLabel control={<Checkbox/>}
label={props.text}
sx={{ width: 1 }}
// handleChange is defined above this function
onChange={(event) => handleChange(props, event)}/>
);
};
The problem is, when I am writing my test, I can render the component, but it does not take onChange as a prop, and does not have a way to mock the handleChange function:
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'
import FilterCheckbox from './FilterCheckbox';
describe('FilterCheckbox', () => {
test('toggles checked and unchecked', () => {
const tagName='Test label'
const tagId='1'
render(<FilterCheckbox text={tagName} key={tagId} tagId={tagId}/>);
const checkbox = screen.getByRole('checkbox');
expect(checkbox.checked).toBeFalsy()
fireEvent(
checkbox,
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
)
// Test fails here, because "handleChange" has not completed its message to the server
expect(checkbox.checked).toBeTruthy()
})
})
My test is failing because of some internal logic in the handleChange function that calls to the server cannot complete, because there is no server for it to reach during the test. I'd like to mock out this logic, but I'm not sure how to do that, given that it's encapsulated in this component and isn't exposed as a prop. Is there a way with Jest to mock out this internal function? Or to provide a response to it from a mocked "server" so that it finishes running and this test can pass?
In case you'd like to suggest ideas on a mocked "server," here is the handleChange function:
import filterSocket from '../SupportingModules/FilterSocket';
const handleChange = (props, event) => {
const checkboxLabel = props.text;
const socket = filterSocket; // Websocket connection
socket.send(JSON.stringify({
'type':'filterChange',
'filterName': checkboxLabel,
'filterId': props.tagId,
'filterState': (event.target.checked ? "on" : "off")
}));
};
The error I am getting when trying to run my test is:
InvalidStateError: Still in CONNECTING state.
Full FilterCheckbox.js file:
import React from 'react';
import { FormControlLabel, Checkbox } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import filterSocket from '../SupportingModules/FilterSocket';
/**
* Detects when a FilterCheckbox is clicked, and sends a message to backend with updated state of filter.
*
* @param {object} props Contains props passed to the component.
* @param {string} props.text String label of the checkbox; matches name of tag to filter.
* @param {number} props.tagId The numerical tag ID to filter for when this box is checked.
* @param {event} event Detects when box has been clicked (checked or unchecked).
*/
const handleChange = (props, event) => {
const checkboxLabel = props.text;
const socket = filterSocket;
const selectionMessage = "FilterCheckbox: " + (event.target.checked ? "" : "un") + "checked " + props.tagId;
console.log(selectionMessage);
socket.send(JSON.stringify({
'type':'filterChange',
'filterName': checkboxLabel,
'filterId': props.tagId,
'filterState': (event.target.checked ? "on" : "off")
}));
};
/**
* Renders a checkbox that allows (de)selection of a tag for image filtering.
*
* @param {object} props Contains props passed to the component.
* @param {string} props.text String label of the checkbox; matches name of tag to filter.
* @param {number} props.tagId The numerical tag ID to filter for when this box is checked.
* @returns The FilterCheckbox component to be rendered in the app.
*/
function FilterCheckbox(props) {
const theme = useTheme();
const largeScreen = useMediaQuery(theme.breakpoints.up('md'));
const largeScreenStyle = {transform: "scale(1.15)", margin: "0 0 0 3%"};
const smallScreenStyle = {transform: "scale(1.75)", margin: "0 1% 0 2.5%"};
return(
<FormControlLabel control={<Checkbox style={ largeScreen ? largeScreenStyle : smallScreenStyle }/>}
label={props.text}
componentsProps={{ typography: {fontSize: {'xs': 40, 'md': 25}}}}
sx = {{ width: 0.98 }} // ensures that one item takes up one row
onChange={(event) => handleChange(props, event)}/>
);
};
export default FilterCheckbox;
And FilterSocket.js:
const filterSocket = new WebSocket(
'ws://' + window.location.host + '/ws/filter-socket/'
);
filterSocket.onopen = () => {
console.log('FilterSocket connected!');
filterSocket.send(JSON.stringify({'type': 'message', 'message': 'FilterSocket Connected!'}));
};
filterSocket.onclose = function(e) {
console.log('Filter socket has closed unexpectedly')
};
export default filterSocket;
As you stated in the comments, the problem is not really with your expectation but with your code generating an error due to the call to socket.send
.
You can avoid that by mocking your socket
module so that the real call is not performed in your test. In order to accomplish that it is enough to add the line jest.mock('../SupportingModules/FilterSocket')
after the imports on your test.
Calling jest.mock only with the first argument automocks all the module exported methods for the current test file.
This way you could also check that when a checkbox is checked the information is sent through the socket:
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import filterSocket from '../SupportingModules/FilterSocket';
import FilterCheckbox from './FilterCheckbox';
jest.mock('../SupportingModules/FilterSocket', () => ({
onopen: jest.fn(),
onclose: jest.fn(),
send: jest.fn(),
close: jest.fn()
}));
describe('FilterCheckbox', () => {
test('toggles checked and unchecked', () => {
const tagName='Test label'
const tagId='1'
render(<FilterCheckbox text={tagName} key={tagId} tagId={tagId}/>);
const checkbox = screen.getByRole('checkbox');
expect(checkbox.checked).toBeFalsy()
fireEvent(
checkbox,
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
)
expect(checkbox.checked).toBeTruthy();
expect(filterSocket.send).toHaveBeenCalled();
})
})