Testing a React component that accepts a useState function as a prop - reactjs

I'm working on a test that is passed a useState function from the calling page
<InlineEdit
text={measure.name}
onSetText={(text) => setMeasure({ ...measure, name: text })}
/>
When I create this in test, I'm not sure how to create that function. I'm stubbing something like below, but obviously teh function isn't going to do anything.
setMeasure is defined with
const [measure, setMeasure] = useState<Measure>({ id: "", name: "" });
My test is simply
test("Renders the InlineEdit 'View' span when state is false", () => {
Enzyme.configure({ adapter: new Adapter() })
const baseProps = {
text: "Name",
onSetText: () => {() => {}}
};
const wrapper = render(<InlineEdit {...baseProps}/>);
...
});
Any suggestions would be greatly appreciated.

Related

Trying to mock MUI's useMediaQuery hook behaviour in Enzyme test

EDIT: I was able to determine that MUI's instructions work correctly when using RTL. This issue is only taking place in Enzyme tests!
I'm following MUI's documentation on how to test useMediaQuery, but I am confused as to whether or not the way I am using useMediaQuery (outlined here in MUI's docs) in my component is compatible with the testing instructions in MUI's docs.
Here's the code in my component:
import { useTheme } from '#mui/material/styles';
import useMediaQuery from '#material-ui/core/useMediaQuery';
const List = () => {
const theme = useTheme();
const isDownLargeBreakpoint =
useMediaQuery(theme.breakpoints.down('lg'));
...
{isDownLargeBreakpoint && (
<ul className="list">
// list items
</ul>
)}
}
The useMediaQuery hook works as expected when I run my app locally, it correctly toggles between true and false when I resize the screen below/above MUI's theme lg breakpoint.
When I try to run my test with the recommended method of setup, despite the window.innerWidth falling below what would satisfy useMediaQuery to return a value of true I always get false in my test. Perhaps it's because I'm not rerendering my component from within my test? Or do I have to do something more in my it clause to trigger what is needing to happen?
Here's the block of code using css-mediaquery recommended by MUI as well as this post which was already answered:
import mediaQuery from 'css-mediaquery';
function createMatchMedia(width) {
return (query) => ({
matches: mediaQuery.match(query, {
width,
}),
addListener: () => {},
removeListener: () => {},
});
}
describe('MyTests', () => {
beforeAll(() => {
window.matchMedia = createMatchMedia(window.innerWidth);
});
});
Here's how I've organized my test file:
import React from 'react';
import { shallow } from 'enzyme';
import mediaQuery from 'css-mediaquery';
import SessionStore from 'app/stores/SessionStore';
import CustomFields from '../CustomFields';
import CustomFieldButton from '../CustomFieldButton';
import PrepareFieldsList from '../PrepareFieldsList';
describe('PrepareFieldsList Component', () => {
let wrapper;
function createMatchMedia(width) {
return (query) => ({
matches: mediaQuery.match(query, {
width,
}),
addListener: () => {},
removeListener: () => {},
});
}
beforeAll(() => {
window.matchMedia = createMatchMedia(window.innerWidth);
});
const defaultProps = {
customFields: [
{
data: null,
id: 'fieldId',
name: '',
required: false,
value: 'test',
},
],
};
beforeEach(() => {
jest.spyOn(SessionStore, 'getSession').mockReturnValue({
hasFeature: () => true,
});
wrapper = shallow(<PrepareFieldsList {...defaultProps} />);
});
...
it('should render CustomFieldButton and CustomFields when hasFeature is true', () => {
expect(wrapper.find(CustomFieldButton)).toHaveLength(1);
expect(wrapper.find(CustomFields)).toHaveLength(1);
});
});
I think it's because innerWidth is not defined in window in jest and you need to set it's value manually, so you can pass value in createMatchMedia like : createMatchMedia(1000) or set value to window.innerWidth like this :
Object.defineProperty(window, 'innerWidth', {writable: true, configurable: true, value: 500})
and in your example would be sth like this :
beforeAll(() => {
Object.defineProperty(window, 'innerWidth', {writable: true, configurable: true, value: 500})
window.matchMedia = createMatchMedia(window.innerWidth);
});
I figured it out. When doing this in Enzyme, you need to mock the hook and its return value with jest.
At the top of the test file:
import useMediaQuery from '#material-ui/core/useMediaQuery';
The mock at the top of the file, below the imports:
jest.mock('#material-ui/core/useMediaQuery');
Then to update the mock value:
useMediaQuery.mockReturnValueOnce(true);
Make sure to do this before you render your component in each test case. So like:
it('should render ChildComponent when useMediaQuery is true', () => {
useMediaQuery.mockReturnValueOnce(true);
const wrapper = shallow(<ParentComponent />);
expect(wrapper).toContainExactlyOneMatchingElement(ChildComponent);
});

ReactJS: every subsequent react testing library test after first one fails

I'm new to React. Somehow only my first test succeeds, and the rest fail. Even if I make the second test same as the first one, it still fails. If I comment out the first test, the second one succeeds, and then the rest fail with component not being found via the id and not present in the DOM.
Here are my tests:
describe('Given SomeComponent component', () => {
configure({ testIdAttribute: 'id' });
describe('When trying to render with only id prop set', () => {
const property = {
id: '1',
} as Property;
render(<SomeComponent prop={property} />);
it('Should render successfully', () => {
expect(screen.getAllByTestId('chart-1')).toBeTruthy();
});
});
describe('When trying to render without data prop set', () => {
const property = {
id: '1',
name: 'property of test',
} as Property;
render(<SomeComponent prop={property} />);
it('Should render successfully', () => {
expect(screen.getAllByTestId('chart-1')).toBeTruthy();
});
});
});
Tested component: SomeComponent
export interface Prop {
property: Property;
}
export const SomeComponent = ({ property }: Prop): JSX.Element => {
return (
<>
<div id={`chart-${curveProperty.id}`}>haha</div>
</>
);
};
Try this:
describe("Given SomeComponent component", () => {
configure({ testIdAttribute: "id" });
it("Should render successfully", () => {
const property = {
id: "1",
} as Property;
const { rerender } = render(<SomeComponent prop={property} />);
expect(screen.getAllByTestId("chart-1")).toBeTruthy();
const newProperty = {
id: "1",
name: "property of test",
} as Property;
rerender(<SomeComponent prop={newProperty} />);
expect(screen.getAllByTestId("chart-1")).toBeTruthy();
});
});
The solution was to use jest test instead of it and it started to work just I wanted it to.
In case switching from it to test doesn't work as it didn't for me:
If the <SomeComponent/> is slightly more complex there could be things still happening at the moment you call getAllByTestId. In my case - switching to the findAllByTestId did the job and all tests are passing as expected.

Jest - Testing modals in React gives error

I am using react-test-renderer with Jest to test react components. But if I test a react-mui modal dialog like this:
describe('Dashboard', function () {
let dashboard;
beforeEach(async () => {
testRenderer = TestRenderer.create(<MemoryRouter><Route component={Dashboard} /></MemoryRouter>);
dashboard = testRenderer.root.findByType(Dashboard);
await waitForExpect(() => expect(dashboard.instance.state.hasLoaded).toBeTruthy());
});
it('opens dialog on clicking the new class', async () => {
const button = testRenderer.root.findByType(Button);
expect(dashboard.instance.state.showDialog).toBeFalsy();
button.props.onClick();
expect(dashboard.instance.state.showDialog).toBeTruthy();
});
});
But, then I get an error:
Error: Failed: "Error: Uncaught 'Warning: An invalid container has
been provided. This may indicate that another renderer is being used
in addition to the test renderer. (For example, ReactDOM.createPortal
inside of a ReactTestRenderer tree.) This is not supported.%s'
How should I test then react portals to make this test work?
Try putting this in your tests:
beforeAll(() => {
ReactDOM.createPortal = jest.fn((element, node) => {
return element
})
});
Based on Oliver's answer, but for TypeScript users:
describe("Tests", () => {
const oldCreatePortal = ReactDOM.createPortal;
beforeAll(() => {
ReactDOM.createPortal = (node: ReactNode): ReactPortal =>
node as ReactPortal;
});
afterAll(() => {
ReactDOM.createPortal = oldCreatePortal;
});
});
For me, the existing solutions don't address the root cause.
I needed to add jest mocks for all the sub-components in the component I was testing.
For example, consider this JSX that I want to test:
import { CustomTextInput } from 'components/CustomTextInput';
import { CustomButton } from 'components/CustomButton';
return (
<>
<CustomTextInput />
<CustomButton />
</>
)
I need to add mocks for CustomTextInput and CustomButton in my test file like this:
jest.mock(
'components/CustomTextInput',
() => ({ default: 'mock-CustomTextInput' }),
);
jest.mock(
'components/CustomButton',
() => ({ default: 'mock-CustomButton' }),
);

Testing onChange function in Jest

I'm relatively new to Jest and testing in general. I have a component with an input element:
import * as React from "react";
export interface inputProps{
placeholder: string;
className: string;
value: string;
onSearch: (depID: string) => void;
}
onSearch(event: any){
event.preventDefault();
//the actual onclick event is in another Component
this.props.onSearch(event.target.value.trim());
}
export class InputBox extends React.Component<inputProps, searchState> {
render() {
return (
<input
onChange={this.onSearch} //need to test this
className={this.props.className}
type="text"
value={this.props.value}
placeholder={this.props.placeholder} />
);
}
}
I want a test that checks that input element's onChange is a function that takes in the input element's value attribute as the parameter. This is how far I have gotten so far:
//test to see the input element's onchange
//returns a function that takes its value as a param
it("onChange param is the same value as the input value", () => {
const mockFn = jest.fn();
const input = enzyme.shallow(<InputBox
value="TestVal"
placeholder=""
className=""
onSearch={mockFn}/>);
input.find('input').simulate('change', { preventDefault() {} });
expect(mockFn.mock.calls).toBe("TestVal");
});
I am going off of the first solution here Simulate a button click in Jest
And: https://facebook.github.io/jest/docs/en/mock-functions.html
Edit: Running the above throws the following error:
TypeError: Cannot read property 'value' of undefined
Syntax on your code snippet I think should be:
import React from 'react';
export default class InputBox extends React.Component {
onSearch(event) {
event.preventDefault();
this.props.onSearch(event.target.value.trim());
}
render () { return (<input onChange={this.onSearch.bind(this)} />); }
}
The test is failing because, as same you define the preventDefault function on the event object, you also must define other properties used on the onSearch function.
it('should call onChange prop', () => {
const onSearchMock = jest.fn();
const event = {
preventDefault() {},
target: { value: 'the-value' }
};
const component = enzyme.shallow(<InputBox onSearch={onSearchMock} />);
component.find('input').simulate('change', event);
expect(onSearchMock).toBeCalledWith('the-value');
});
Previous test code needs to define the event shape because you are using shallow rendering. If you want instead to test that the actual input value is being used on your onSearch function you need to try a full render with enzyme.mount:
it('should call onChange prop with input value', () => {
const onSearchMock = jest.fn();
const component = enzyme.mount(<InputBox onSearch={onSearchMock} value="custom value" />);
component.find('input').simulate('change');
expect(onSearchMock).toBeCalledWith('custom value');
});
For those testing using TypeScript (and borrowing from the answers above), you'll need to perform a type coercion (as React.ChangeEvent<HTMLInputElement>) to ensure that the linter can view the signature as being compatible:
React file
export class InputBox extends React.Component<inputProps, searchState> {
onSearch(event: React.ChangeEvent<HTMLInputElement>){
event.preventDefault();
//the actual onclick event is in another Component
this.props.onSearch(event.target.value.trim());
}
render() {
return (
<input
onChange={this.onSearch} //need to test this
className={this.props.className}
type="text"
value={this.props.value}
placeholder={this.props.placeholder} />
);
}
}
Test file
it('should call onChange prop', () => {
const onSearchMock = jest.fn();
const event = {
target: { value: 'the-value' }
} as React.ChangeEvent<HTMLInputElement>;
const component = enzyme.shallow(<InputBox onSearch={onSearchMock} />);
component.find('input').simulate('change', event);
expect(onSearchMock).toBeCalledWith('the-value');
});
or alternatively
it('should call onChange prop', () => {
const onSearchMock = jest.fn();
const event = {
target: { value: 'the-value' }
} as React.ChangeEvent<HTMLInputElement>;
const component = enzyme.mount<InputBox>(<InputBox onSearch={onSearchMock} />);
const instance = component.instance();
instance.onSearch(event);
expect(onSearchMock).toBeCalledWith('the-value');
});
I figured out the solution.
So, instead of passing in the value inside InputBox, we have to pass it inside the second param of simulate as shown below. Then we simply check for equality against the first arg of the first call to the mockFn. Also, we can get rid of the event.preventDefault();
it("onChange param is the same value as the input element's value property", () => {
const mockFn = jest.fn();
const input = enzyme.shallow(<InputBox
value=""
placeholder=""
className=""
onSearch={mockFn}/>);
input.find('input').simulate('change', {target: {value: 'matched'} });
expect(mockFn.mock.calls[0][0]).toBe('matched');
});
How about this one? I simulate the change event using enzyme and perform a snapshot test.
Component
import React, { FunctionComponent, useState } from 'react';
const Index: FunctionComponent = () => {
const [val, setVal] = useState('');
const onInputChange = e => {
e.preventDefault();
setVal(e.target.value);
};
return (
<input type='text' onChange={onInputChange} value={val} />
);
};
export default Index;
Unit Test
describe('Index with enzyme', () => {
it('Should set value to state when input is changed', () => {
const container = shallow(<Index />);
const input = container.find('input');
input.simulate('change', { preventDefault: jest.fn, target: { value: "foo" } });
expect(container).toMatchSnapshot();
});
});
Snapshot
exports[`Index with enzyme Should set value to state when input is changed 1`] = `
<input
onChange={[Function]}
type="text"
value="foo"
/>
`;
I struggled with this for hours. Plus since I had multiple select fields on one page. What I found is that Textfield solution works differently from Select.test given on docs.
On the code I defined SelectProps with id. (You can also go with data-testid)
I could only trigger dropdown by clicking this field.
<TextField
select
variant = "outlined"
value = { input.value || Number(0) }
onChange = { value => input.onChange(value) }
error = { Boolean(meta.touched && meta.error) }
open = { open }
SelectProps = {
{
id: `${input.name}-select`,
MenuProps: {
anchorOrigin: {
vertical: "bottom",
horizontal: "left"
},
transformOrigin: {
vertical: "top",
horizontal: "left"
},
getContentAnchorEl: null
}
}
}
{ ...props} >
//yourOptions Goes here
</TextField>
And in my test.
const pickUpAddress = document.getElementById("address-select");
UserEvent.click(pickUpAddress);
UserEvent.click(screen.getByTestId("address-select-option-0"));
Worked like a charm afterwards. Hope this helps.
If you're writing lwc (salesforce) jest tests you can simulate this by selecting the input and dispatching an event.
const changeEvent = new CustomEvent('change', {
detail: {
'value': 'bad name'
}
});
element.shadowRoot.querySelector('lightning-input').dispatchEvent(changeEvent);

testing onClick with react-navigation

I'm using Jest with Enzyme, and I have this component which includes a navigate method call:
export class LeadList extends React.Component {
render() {
const { navigate } = this.props.navigation;
return (
<List>
{this.props.data.allLeads.map((lead, i) => {
return (
<ListItem
key={i}
onPress={() =>
navigate('Details', lead.id)
}
/>
// ...
</ListItem>
)})}
</List>
);
}
}
I'm trying to test that it gets called properly, so I threw this together:
const testProps = props => ({
data: {
allLeads: [
{id: 1, name: 'John Doe'},
{id: 2, name: 'Jane Doe'}
],
loading: false,
},
navigation: jest.fn((options, callback) => callback('Details', 1)),
...props,
})
describe('interactions', () => {
let props
let wrapper
beforeEach(() => {
props = testProps()
wrapper = shallow(<LeadList {...props} />)
})
describe('clicking a lead', () => {
beforeEach(() => {
wrapper.find(ListItem).first().prop('onPress')
})
it('should call the navigation callback', () => {
expect(props.navigation).toHaveBeenCalledTimes(1)
})
})
})
Output is:
Expected mock function to have been called one time, but it was called zero times.
What's the right way to handle this? Do I need to use a spy?
EDIT:
I'm getting the same when I change it like so:
const testProps = props => ({
// ...
navigation: {navigate: jest.fn()},
...props,
})
it('should call the navigation callback', () => {
expect(props.navigation.navigate).toHaveBeenCalledTimes(1)
})
Output:
expect(jest.fn()).toHaveBeenCalledTimes(1)
Expected mock function to have been called one time, but it was called zero times.
at Object.<anonymous> (__tests__/LeadList-test.js:48:35)
at tryCallTwo (node_modules/promise/lib/core.js:45:5)
at doResolve (node_modules/promise/lib/core.js:200:13)
at new Promise (node_modules/promise/lib/core.js:66:3)
at Promise.resolve.then.el (node_modules/p-map/index.js:46:16)
at tryCallOne (node_modules/promise/lib/core.js:37:12)
at node_modules/promise/lib/core.js:123:15
You will need a spy to test this. Here is an example test for finding the ForgotPassword button on a LoginScreen and testing that it navigates to the correct screen.
test('Press Forgot Password Button', () => {
const spy = jest.spyOn(navigation, 'navigate')
const wrapper = shallow(
<LoginScreen
navigation={navigation}
error={{}}
onLogin={jest.fn()}
/>,
)
const forgotButton = wrapper.find('Button').at(0)
forgotButton.props().onPress()
expect(spy).toBeCalledWith('ForgotPassword')
})
The prop navigation that is passed to the component is not a function. It's an object that contains a function called navigate.
Ironically, that's exactly what you're using in your component code:
const { navigate } = this.props.navigation;
And so, you'll have to change the navigation prop that you're passing from the test to be:
navigation: {navigate: jest.fn()}
and then in your test:
expect(props.navigation.navigate).toHaveBeenCalledTimes(1)
Edit:
In order to actually get the function to be called, you'll have to simulate a press. Right now the code finds the onPress function, but doesn't invoke it.
To do this you can replace
wrapper.find(ListItem).first().prop('onPress')
with
wrapper.find(ListItem).first().props().onPress()

Resources