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

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);
});

Related

Jest throws "Types of property 'id' are incompatible" error when it is trying to use stories from storybook using typescript

So..I am trying to use stories for my unit test with jest + RTL to minimize duplication (explained here) and the test throws "Types of property 'id' are incompatible" error when I pass the args that is also used in my story.
I am currently using below packages as my testing suite.
"#storybook/react": "^6.5.9",
"#storybook/testing-react": "^1.2.4",
"#testing-library/react": "^13.3.0",
"jest": "^28.1.1",
// and others...
Below is my test.
import * as React from 'react';
import { screen, render } from '#testing-library/react';
import { Default as Checkbox } from '../../stories/AccountCheckbox.stories';
const renderCheckbox = () => {
render(<Checkbox {...Checkbox.args} />); // the point where it throws an type error that says 'id is incompatible'.
}
describe('<AccountCheckbox />', () => {
test('should display account information', () => {
renderCheckbox();
expect(screen.getByTestId('example-test-id')).toBeInTheDocument();
});
});
Below is my story.
import React from 'react';
import { ComponentMeta, ComponentStory } from '#storybook/react';
import AccountCheckbox from '../components/AccountCheckbox';
import { StoryAppWrapper } from '../../.storybook/wrapper/StoryAppWrapper';
export default {
title: 'AccountCheckbox',
component: AccountCheckbox,
decorators: [(Story) => <StoryAppWrapper>{Story()}</StoryAppWrapper>]
} as ComponentMeta<typeof AccountCheckbox>;
const Template: ComponentStory<typeof AccountCheckbox> = (args) => (
<AccountCheckbox {...args} />
);
export const Default = Template.bind({});
// These are the args I am trying to utilize in my test as well.
Default.args = {
id: 'example-id',
testId: 'example-test-id',
label: 'Account 1',
selected: false,
onChange: () => { },
};
Below is the type for the checkbox I declared.
export type CheckboxProps = {
id: string;
testId: string;
label: string;
selected: boolean;
onChange: any;
};
Below is the actual code.
import * as React from 'react';
import { Checkbox } from 'custom-lib';
import { CheckboxProps } from '../../types/types';
const AccountCheckbox = ({
id,
testId,
label,
selected,
onChange,
}: CheckboxProps): JSX.Element => {
return (
<Checkbox
id={id}
data-testid={testId}
label={label}
state={selected ? 'true' : 'false'}
onChange={onChange}
/>
);
};
export default AccountCheckbox;
Can anyone pinpoint what I've missed within these codes?
You're trying to render the Story, not the Checkbox itself. Why not import the checkbox directly, but use the story args too?
import * as React from 'react';
import { screen, render } from '#testing-library/react';
import AccountCheckbox from '../components/AccountCheckbox';
import { Default as AccountCheckboxStory } from '../../stories/AccountCheckbox.stories';
const renderCheckbox = () => {
render(<AccountCheckbox {...AccountCheckboxStory.args} />);
}
describe('<AccountCheckbox />', () => {
test('should display account information', () => {
renderCheckbox();
expect(screen.getByTestId('example-test-id')).toBeInTheDocument();
});
});
You may need to adjust paths to the Story/AccountCheckbox as I'm not quite sure what your directory structure looks like.
Update
Going from your comment and the link, it seems that they are using some form of a compositional function to wrap the exported stories and return them as components. So in your case, would you not do this:
import React from 'react';
import { render, screen } from '#testing-library/react';
import { composeStories } from '#storybook/testing-react';
import * as stories from '../../stories/AccountCheckbox.stories';
const { Default: AccountCheckBoxDefault } = composeStories(stories);
test('renders profile page', async () => {
render(<AccountCheckBoxDefault />);
// ... some assertions here
});
describe('<AccountCheckbox />', () => {
test('should display account information', () => {
renderCheckbox();
expect(screen.getByTestId('example-test-id')).toBeInTheDocument();
});
});
I don't think it's actually necessary to pass in the args, as those args should be used by the story itself.

Writing a unit test to check if the app is offline in react-native

I have the following component which displays a text on the app if the app is offline.
import React from 'react';
import { useNetInfo } from '#react-native-community/netinfo';
import { Label } from 'components/ui';
const OfflineNotice = () => {
const netInfo = useNetInfo();
if (netInfo.type !== 'unknown' && netInfo.isInternetReachable === false) {
return <Label size={18} text='No Internet Connection' />;
}
return null;
};
export default OfflineNotice;
I want to write a unit test to this to check if this works properly. How can I do this?
Im new to unit tests. I don't understand how to mock this.
I use typescript and testing-library/react-native.
UPATED:
Why does this first test fail? It should NOT TO BE NULL. But it fails. The error is,
OfflineNotice component › test
expect(received).not.toBeNull()
Received: null
15 |
16 | const { queryByText } = render(<OfflineNotice />);
> 17 | expect(queryByText(/no internet connection/i)).not.toBeNull();
| ^
18 | });
19 |
at Object.<anonymous> (src/components/offline-notice/offline-notice.test.tsx:17:56)
Cruising the react-native-netinfo github repo, troubleshooting section
You should then add the following to your Jest setup file to mock the
NetInfo Native Module:
import mockRNCNetInfo from '#react-native-community/netinfo/jest/netinfo-mock.js';
jest.mock('#react-native-community/netinfo', () => mockRNCNetInfo);
Their mock for testing is:
const defaultState = {
type: 'cellular',
isConnected: true,
isInternetReachable: true,
details: {
isConnectionExpensive: true,
cellularGeneration: '3g',
},
};
const RNCNetInfoMock = {
configure: jest.fn(),
fetch: jest.fn(),
addEventListener: jest.fn(),
useNetInfo: jest.fn(),
};
RNCNetInfoMock.useNetInfo.mockResolvedValue(defaultState);
Given this I think you could craft your own mock resolved values in each unit test case:
import { useNetInfo } from '#react-native-community/netinfo';
jest.mock('#react-native-community/netinfo', () => {
useNetInfo: jest.fn(),
});
...
// Happy path test, known type and internet unreachable
useNetInfo.mockResolvedValueOnce({
type: 'test', // not 'unknown'
isInternetReachable: false,
});
// assert render non-null
const { queryByText } = render(<OfflineNotice />);
expect(queryByText(/no internet connection/i)).not.toBeNull();
...
// Sad path test, known type and internet reachable
useNetInfo.mockResolvedValueOnce({
type: 'test', // not 'unknown'
isInternetReachable: true,
});
// assert render null
const { queryByText } = render(<OfflineNotice />);
expect(queryByText(/no internet connection/i)).toBeNull();
...
// Sad path test, unknown type and internet unreachable
useNetInfo.mockResolvedValueOnce({
type: 'unknown',
isInternetReachable: false,
});
// assert render null
const { queryByText } = render(<OfflineNotice />);
expect(queryByText(/no internet connection/i)).toBeNull();
...
// Sad path test, unknown type and internet reachable
useNetInfo.mockResolvedValueOnce({
type: 'test', // not 'unknown'
isInternetReachable: true,
});
// assert render null
const { queryByText } = render(<OfflineNotice />);
expect(queryByText(/no internet connection/i)).toBeNull();
React-Native-Testing-Library
React-Testing-Library
Query Cheetsheet
Text Match Options
Now my tests are working. But it gives the unexpected outputs.
COMPONENT:
import React from 'react';
import { useNetInfo } from '#react-native-community/netinfo';
import { Label } from 'components/ui';
import strings from './offline-notice.strings';
import styles from './offline-notice.styles';
const OfflineNotice = ({ style, text }: IProps) => {
const netInfo = useNetInfo();
if (netInfo.type !== 'unknown' && netInfo.isInternetReachable === false) {
return <Label size={18} style={[styles.label, style]} text={text} />;
}
return null;
};
OfflineNotice.defaultProps = {
text: strings.NO_INTERNET_CONNECTION,
};
interface IProps {
style?: Object;
text?: string;
}
export default OfflineNotice;
TEST:
import { render } from '#testing-library/react-native';
import React from 'react';
import { useNetInfo } from '#react-native-community/netinfo';
import OfflineNotice from './offline-notice.component';
import strings from './offline-notice.strings';
describe('OfflineNotice component', () => {
it('should display the message if internet is not reachable', () => {
useNetInfo.mockResolvedValueOnce({
type: 'test',
isInternetReachable: false,
});
const { getByText } = render(<OfflineNotice text={strings.NO_INTERNET_CONNECTION} />);
expect(getByText(strings.NO_INTERNET_CONNECTION)).not.toBeNull();
});
it('should not display the message if internet is reachable', () => {
useNetInfo.mockResolvedValueOnce({
type: 'test',
isInternetReachable: true,
});
const { getByText } = render(<OfflineNotice text={strings.NO_INTERNET_CONNECTION} />);
expect(getByText(strings.NO_INTERNET_CONNECTION)).toBeNull();
});
});
Jest.Setup.ts
import mockRNCNetInfo from '#react-native-community/netinfo/jest/netinfo-mock.js';
jest.mock('#react-native-community/netinfo', () => mockRNCNetInfo);
When running the test, following output is given. What am I doing wrong here?
As #Drew said, I set the configuration according by documentation, but I did a little difference in mock for testing.
For me, I did this and worked:
jest.mock('#react-native-community/netinfo', () => ({
...jest.requireActual('#react-native-community/netinfo'),
useNetInfo: () => ({
isConnected: true,
})
}));

ReactWrapper can only wrap valid elements (material-ui - enzyme)

I'm writing a test for a component to test one of its functions, but I am getting an error: ShallowWrapper can only wrap valid elements
The component file is as follows - TextInput.js:
/* eslint-disable react/require-default-props */
import React from 'react'
import PropTypes from 'prop-types'
import { InputLabel, TextField } from '#material-ui/core'
const TextInput = ({
name, label, onChange, placeholder, value, error, optional = false, isDisable = false, t,
}) => (
<>
{label ? (
<InputLabel htmlFor={name} className="default-style_label">
{label}
{' '}
{optional && <span className="optional">{`(${t('application.optional')})`}</span>}
</InputLabel>
) : ''}
<TextField
type="text"
name={name}
placeholder={placeholder}
value={value}
// isRequired={isRequired}
disabled={isDisable}
onChange={onChange}
error={error && true}
helperText={error}
variant="outlined"
className="default-style_input"
/>
</>
)
TextInput.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string,
onChange: PropTypes.func.isRequired,
placeholder: PropTypes.string,
value: PropTypes.string,
error: PropTypes.string,
optional: PropTypes.bool,
isDisable: PropTypes.bool,
t: PropTypes.func,
}
export default TextInput
The test file
/* eslint-disable no-undef */
import React from 'react'
import { shallow, mount } from 'enzyme'
import TextInput from '../TextInput'
function createTestProps(props) {
return {
// common props
name: 'test',
label: 'foo',
value: 'bar',
onChange: jest.fn(),
// allow to override common props
...props,
}
}
describe('rendering', () => {
describe('<TextInput>', () => {
let wrapper
let instance
beforeEach(() => {
const props = createTestProps()
wrapper = mount(shallow(<TextInput {...props} />)).get(0)
instance = wrapper.instance()
})
afterEach(() => {
jest.clearAllMocks()
})
it('should be rendered', () => {
const content = wrapper.find('input').at(1)
console.debug(content.debug())
console.log(instance)
expect(content.value).toBe('bar')
})
})
})
The problem is that my tests fail when remove mount from
wrapper = mount(shallow(<TextInput {...props} />)).get(0)
with a Compared values have no visual difference
Any ideas why this is happening would be much appreciated!
You should use either mount or shallow to render your component based on your use case.
use mount if you want to render your component where all children would be rendered to the last leaf node.
use shallow if you need a level deep rendering of your component.
Note: in most cases, you should use shallow rendering as mount takes a long time.
You should use shallow or mount, not both of them.
wrapper = mount(<TextInput {...props} />);
const content = wrapper.find('input');
expect(content.value).toBe('bar');
It's something different when you're using #material-ui.
You've to use #material-ui's Built-in API(s). Such as createMount, createShallow, createRender in order to use enzyme's shallow, mount & render.
These APIs are built on top of enzyme, so you can't use enzyme directly for testing #material-ui.
Here's my test file
/* eslint-disable no-undef */
import React from 'react'
import { createMount } from '#material-ui/core/test-utils'
import TextField from '#material-ui/core/TextField'
import TextInput from '../TextInput'
function createTestProps(props) {
return {
// common props
name: 'test',
label: 'foo',
value: 'bar',
onChange: jest.fn(),
// allow to override common props
...props,
}
}
describe('rendering', () => {
describe('<TextInput>', () => {
let mount
let props
beforeEach(() => {
mount = createMount()
props = createTestProps()
})
afterEach(() => {
mount.cleanUp()
})
it('renders a <TextField/> component with expected props', () => {
const wrapper = mount(<TextInput {...props} />)
expect(wrapper.props().name).toEqual('test')
expect(wrapper.props().onChange).toBeDefined()
})
it('should trigger onChange on <TextField/> on key press', () => {
const wrapper = mount(<TextInput {...props} />)
wrapper.find('input').simulate('change')
expect(props.onChange).toHaveBeenCalled()
})
})
})
I found the solution here
Material UI + Enzyme testing component

React Jest Test coverage showing untested line (default props)

I am running a test on the simple button component. All works fine, however, in the test coverage I am getting an untested line (21: onClick: () => { return true }). This line points to the default props function. How can I make sure that I have covered the unit test for this line?
My component:
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
export const Button = (props) => {
return (
<Link
to={props.link}
className={props.classes}
onClick={props.onClick}
>
{props.title}
</Link>
);
};
Button.defaultProps = {
link: '/',
title: 'Home',
classes: 'btn btn--primary',
onClick: () => { return true }
}
Button.propTypes = {
link: PropTypes.string,
title: PropTypes.string,
classes: PropTypes.string,
onClick: PropTypes.func
};
export default Button;
My test:
import React from 'react';
import { shallow } from 'enzyme';
import { Button } from '../../../components/partials/Button';
test('should render button', () => {
const wrapper = shallow(<Button link='/test' title='Home' classes='btn btn--primary' onClick={ () => { return true }} />);
expect(wrapper).toMatchSnapshot();
const props = wrapper.props();
expect(props.to).toBe('/test');
expect(props.className).toBe('btn btn--primary');
expect(props.onClick).toBe(props.onClick);
expect(props.children).toBe('Home');
});
test('should have default onClick', () => {
expect(Button.defaultProps.onClick).toBeDefined();
});
You need to run the onClick function in order for it to count for coverage. So, make a test that simulates a click event on the button, which will trigger its onClick function:
it('does something when clicked', () => {
const wrapper = shallow(<Button link='/test' title='Home' classes='btn btn--primary' onClick={ () => { return true }} />);
wrapper.simulate('click');
expect(...);
});

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' }),
);

Resources