1

I have this code:

/* eslint-disable react/display-name */
import { Box, Button, LinearProgress, makeStyles } from '@material-ui/core';
import { Refresh } from '@material-ui/icons';
import { SearchHistoryContext } from 'appContext';
import useSnackBar from 'components/useSnackbar';
import PropTypes from 'prop-types';
import QueryString from 'qs';
import React, { useContext, useEffect, useState } from 'react';
import { formatDate } from 'utils/formatDate';
import http from 'utils/http';

const styles = makeStyles(() => ({
  progress: {
    margin: '-15px -15px 11px -15px',
  },
  button: {
    width: '150px',
  },
  details: {
    fontSize: '15px',
  },
}));

const LogsMainPage = props => {
  const classes = styles();
  const { location, history, match, updateQuery } = props;
  const [displaySnackbar] = useSnackBar();

  const updateData = async () => {
    history.push({
      search: QueryString.stringify(query),
      state: 'compliance-logs',
    });

  };


  const logTableColumn = [
    {
      columns: [
        {
          Header: 'Timestamp',
          id: '_id',
          accessor: c => formatDate(c.timestamp, 'd/MM/yyyy, H:mm:ss'),
          minWidth: 120,
        },
        {
          Header: 'User Email',
          id: 'user_email',
          accessor: c => c.user_email,
          minWidth: 80,
        },
      ],
    },
  ];

  return (
    <div>
      {isLoading && <LinearProgress className={classes.progress} />}
      <Box display="flex" justifyContent="flex-end">
        <Button
          id="refresh-button"
          variant="outlined"
          color="primary"
          className={classes.button}
          disabled={isLoading}
          onClick={updateData}
          startIcon={<Refresh />}
        >
          Refresh
        </Button>
      </Box>
      <Box mb={2} />
    </div>
  );
};

LogsMainPage.propTypes = {
  history: PropTypes.object,
  match: PropTypes.object,
  location: PropTypes.object,
  updateQuery: PropTypes.func,
};

export default LogsMainPage;

Unit Test:

import LogsMainPage from 'containers/Log/LogsMainPage';
import { shallow } from 'enzyme';
import notistack from 'notistack';
import React from 'react';

jest.mock('notistack', () => ({
  useSnackbar: jest.fn(),
}));

const enqueueSnackbar = jest.fn();
jest.spyOn(notistack, 'useSnackbar').mockImplementation(() => {
  return { enqueueSnackbar };
});
jest.mock('react', () => ({
  ...jest.requireActual('react'),
  useContext: () => ({
    searches: {},
  }),
}));

afterEach(() => {
  jest.clearAllMocks();
});

describe('render test', () => {
  const mockCallBack = jest.fn();

  const wrapper = shallow(
    <LogsMainPage history={{ push: jest.fn() }} location={{ search: {} }} />,
  );

  it('renders without crashing', () => {
    expect(wrapper).toHaveLength(1);
  });

  it('renders refresh button without crashing', () => {
    const button = wrapper.find('#refresh-button');

    expect(button).toHaveLength(1);

    button.setProps({ onClick: mockCallBack });
    button.simulate('click');

    expect(mockCallBack).toHaveBeenCalledTimes(1);
  });
});

When I ran my single test (each of expect) it always pass. But if I ran the describe part it always failed.

TypeError: Cannot read property 'get' of undefined the editor shown the error is on const classes = styles() part but all my other unit test using same as this, is passed.

Any solution?

5
  • I have the same problem, when manual mocking makeStyles, creating a theme and binding that theme to the original makeStyles. It's only in one single Component of the tests. Many other components look similar in makeStyles and useStyles usage, but this one Component is not willing to cooperate. I'm looking for the reason. Commented May 5, 2021 at 16:45
  • I've fixed mine mocking the makeStyles and making sure I call the makeStyles callback function (with a created theme in my case) so that test coverage can cover the callback function. But in your case, are you sure you can import { makeStyles } from '@material-ui/core'; ? I'm using import { makeStyles } from '@material-ui/core/styles'; Commented May 6, 2021 at 16:24
  • I found the Perfect way to handle all 4 problems with mocking makeStyles and useContext. Because Mui makeStyles result function also uses React.useContext which, when you mock it for your own context, also break it for makeStyles. I can answer it now if you're still in need of this. If not, I might post an Answer soon for others when I have more time. Commented May 9, 2021 at 22:09
  • @KeitelDOG, would you be able to post this answer please? :) Commented Aug 3, 2021 at 22:08
  • 1
    @Emil all right, I posted an answer for why the error is thrown, and how to correctly mock makeStyles and useContext to avoid this error and all possible side errors from this, and get 100% coverage for makeStyles. Commented Aug 4, 2021 at 15:07

2 Answers 2

2

Short Answer: In your case, it's because you mocked useContext BUT makeStyles internally uses useContext, hence it cannot find the context object it was supposed to find and try a get method of an undefined result in the mocked context you provided. So you have to:

  1. Mock makeStyles correctly.
  2. Mock useContext EXCEPT for the Material UI contexts.

Let's dive deeper.

Mocking Material UI makeStyles with simple jest function make you loose test coverage. When making it more complex, it leads to some problems.

But when you fix it, or reverse it, each problem solved leads to another when using shallow rendering (mount will not do it, can be slow on top level components):

  • it lost test coverage when calling useStyles which is now an empty function with no style (const useStyles = makeStyles(theme => {...}))
  • Not mocking it throw error for additional values of a custom theme
  • Binding mocked function argument with the custom theme works, and you can call function argument to fill coverage. But you loose coverage if passing parameters when calling useStyles({ variant: 'contained', palette: 'secondary' }) (result function of makeStyles)
  • Lot of things broke when mocking useContext, because makeStyles result function uses useContext internally.

(example of useStyles parameter handling)

{
  backgroundColor: props => {
    if (props.variant === 'contained') {
      return theme.palette[props.palette].main;
    }
    return 'unset';
  },
}

You should know how to correctly mock makeStyles for maximum testing experience. I managed to solved all of these problems and use manual mock https://jestjs.io/docs/en/manual-mocks (or inline mocking as alternative at the bottom):

Step 1:

I mocked in the core path instead, but both should work: <root>/__mocks__/@material-ui/core/styles.js

// Grab the original exports
// eslint-disable-next-line import/no-extraneous-dependencies
import * as Styles from '@material-ui/core/styles';
import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
import options from '../../../src/themes/options'; // I put the theme options separately to be reusable

const makeStyles = func => {
  /**
   * Note: if you want to mock this return value to be
   * different within a test suite then use
   * the pattern defined here:
   * https://jestjs.io/docs/en/manual-mocks
   */

  /**
   * Work around because Shallow rendering does not
   * Hook context and some other hook features.
   * `makeStyles` accept a function as argument (func)
   * and that function accept a theme as argument
   * so we can take that same function, passing it as
   * parameter to the original makeStyles and
   * bind it to our custom theme, created on the go
   *  so that createMuiTheme can be ready
   */
  const theme = createMuiTheme(options);
  return Styles.makeStyles(func.bind(null, theme));
};

module.exports = { ...Styles, makeStyles };

So basically, this is just using the same original makeStyles and passes it the custom theme on the go which was not ready on time.

Step 2:

makeStyles result uses React.useContext, so we have to avoid mocking useContext for makeStyles use cases. Either use mockImplementationOnce if you use React.useContext(...) at the first place in you component, or better just filter it out in your test code as:

jest.spyOn(React, 'useContext').mockImplementation(context => {
  // only stub the response if it is one of your Context
  if (context.displayName === 'MyAppContext') {
    return {
      auth: {},
      lang: 'en',
      snackbar: () => {},
    };
  }

  // continue to use original useContext for the rest use cases
  const ActualReact = jest.requireActual('react');
  return ActualReact.useContext(context);
});

And on your createContext() call, probably in a store.js, add a displayName property (standard), or any other custom property to Identify your context:

const store = React.createContext(initialState);
store.displayName = 'MyAppContext';

The makeStyles context displayName will appear as StylesContext and ThemeContext if you log them and their implementation will remain untouched to avoid error.

This fixed all kind of mocking problems related to makeStyles + useContext. And in term of speed, it just feels like the normal shallow rendering speed and can keep you away of mount for most use cases.

ALTERNATIVE to Step 1:

Instead of global manual mocking, we can just use the normal jest.mock inside any test. Here is the implementation:

jest.mock('@material-ui/core/styles', () => {
  const Styles = jest.requireActual('@material-ui/core/styles');

  const createMuiTheme = jest.requireActual(
    '@material-ui/core/styles/createMuiTheme'
  ).default;

  const options = jest.requireActual('../../../src/themes/options').default;

  return {
    ...Styles,
    makeStyles: func => {
      const theme = createMuiTheme(options);
      return Styles.makeStyles(func.bind(null, theme));
    },
  };
});

Since then, I also learned to mock useEffect and calling callback, axios global interceptors, etc.

Sign up to request clarification or add additional context in comments.

Comments

0
---Store.js----
import React, { createContext, useReducer } from "react";
import Reducer from "./Reducer";

const initialState = {
  userActivityData: {},
};

const Store = ({ children }) => {
  const [state, dispatch] = useReducer(Reducer, initialState);
  return (
    <Context.Provider value={[state, dispatch]}>{children}</Context.Provider>
  );
};

export const Context = createContext(initialState);
Context.displayName = "MyAppContext";
export default Store;

---Component.js--- using the above context store.

#adding context store in the test file works for me. mocking usecontext will break material styles. ---Component.test.js--- import Store from "../../Utils/Store";

beforeEach(() => {
  wrapper = mount(
    <Store>
      <Component/>
    </Store>
  );
});

Note: This is not the complete code. But the point is, adding context store to the test file solved the problem.

1 Comment

As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.