Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shallow testing (enzyme) component with custom theme and mui-styles' makeStyles hook #15404

Closed
BenBrewerBowman opened this issue Apr 18, 2019 · 15 comments
Labels
out of scope The problem looks valid but we won't fix it (maybe we will revisit it in the future) test

Comments

@BenBrewerBowman
Copy link

BenBrewerBowman commented Apr 18, 2019

Can you add a way (or add documentation) to shallow test a component with a custom theme and using styling hooks?
Using unwrap worked with withStyles to shallow mount a component, but now with hook styling there is no longer a HOC styling wrapper. Dive is also not working as expected.

ex.

const theme = createTheme(/*custom theme here */);

const useStyles = makeStyles(theme => ({
    customStyle: {
          color: /* custom theme value here */
}));

const MyComponent = () => {

    const classes = useStyles();

    return (
        <div className={classes.customStyle} />
    );
}

I'm use shallow rendering (enzyme), and I wrap this component in the themeprovider from mui-styles. Diving into the themeprovider I get an error. Can you either add documentation surrounding this or add a clear way to test styling hooks?

@eps1lon
Copy link
Member

eps1lon commented Apr 18, 2019

Could you share your testing setup and the error you get?

@eps1lon eps1lon added the status: waiting for author Issue with insufficient information label Apr 18, 2019
@BenBrewerBowman
Copy link
Author

BenBrewerBowman commented Apr 18, 2019

const useStyles = makeStyles((theme: any) => ({
    button: {
      // we've added this to the theme 
      color: theme.palette.red[500]
    }
  }));

  const CustomComponent = ({ title }: { title: string}) => {
    const classes = useStyles();
    return (
      <Typography className={classes.button} >
        {title}
      </Typography>
    )
  }

  const title = 'some title';

  const themeWrapper = shallow(
    <ThemeProvider theme={theme}>
      <CustomComponent title={title} />
    </ThemeProvider>
  );

 // I have tried all of the following
 const wrapper = themeWrapper;
 const wrapper = themeWrapper.dive();
 const wrapper = themeWrapper.childAt(0).dive();
 const wrapper = themeWrapper.find(CustomComponent).dive();

  test('displays typography with text', () => {
    expect(wrapper.find('Typography').text()).toEqual(title);
  });

This test fails each time with the error:
Method “text” is meant to be run on 1 node. 0 found instead.

First off am I doing this test correctly. Is there something I'm missing? If not, what is the correct solution to shallow render this example? I'm getting this to pass with mount, but mounting is really driving up our build times.

@oliviertassinari
Copy link
Member

@BenBrewerBowman I believe we already have a discussion for this problem: #11864.

I would encourage you to use the mount API and to forget about the existence of the shallow one, or even to consider using react-testing-library.
I don't think that enzyme shallow API support the context provider pattern yet, e.g. enzymejs/enzyme#2103. React shallow test utils might not support it either: facebook/react#14329.

but mounting is really driving up our build times.

I understand the concern. Did you benchmark it? We are migration the whole Material-UI test suite away from the shallow API. I personally think that the code refactoring flexibility the mount API provides worth a slower test suite. We haven't benchmarked any significant difference yet. The tests on the master branch take 32s, the tests on the next branch, where we started the mount migration take 42s (but we added many new tests).

@oliviertassinari oliviertassinari added duplicate This issue or pull request already exists and removed status: waiting for author Issue with insufficient information test labels Apr 21, 2019
@mdvorscak
Copy link

mdvorscak commented May 30, 2019

@BenBrewerBowman I was running into the same issues and I've found a way around using jest mocks. Here are the steps for my workaround:

  1. Create a manual mock at the root of your project (defined in jest.config). The path should look like <root>/__mocks__/@material-ui/styles.ts
  2. Add the following code into the file:
// Grab the original exports
import * as Styles from "@material-ui/styles";

const makeStyles = () => {
    return () => {
        /**
         * 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
         **/
        return {};
    };
};

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

You should now be able to use shallow rendering on all your tests. Just a couple of notes:

  1. I'm using typescript but you should be able to easily adapt this for JS
  2. Be sure that your mock file path matches what you are importing. In my case our imports always look like this: import { makeStyles } from "@material-ui/styles";

edit: updated mock to be much simpler (inspired by: #14357 (comment))

@oliviertassinari
Copy link
Member

oliviertassinari commented May 31, 2019

@mdvorscak Thanks for sharing, it looks similar to: #14357 (comment).

@alltom
Copy link
Contributor

alltom commented May 31, 2019

@oliviertassinari You're welcome.

@mdvorscak
Copy link

Thanks @oliviertassinari. Maybe this method of mocking makeStyles could be documented somewhere? Note: I also cleaned up my mock code to be as minimal as possible

@oliviertassinari
Copy link
Member

oliviertassinari commented May 31, 2019

@mdvorscak Well, we don't encourage shallow testings, it's not something we want people to rely on. It can be frustrating.
In the reference, I have shared, the use case is for snapshot testing. Those snapshot tests should be easier in v4 as we generate global class names. So, I agree, it can be documented 👍.

I also cleaned up my mock code to be as minimal as possible

Your clean up mock wouldn't cut it for the snapshot use case.

@zsid
Copy link

zsid commented Aug 15, 2019

I have been using @mdvorscak implementation and placing the mock inside <root>/__mocks__/@material-ui/styles.ts. However, when I upgraded to the new react scripts and jest everything started failing.

I solved that by moving the mock inside src. I wonder if that change is a feature or a bug 😄

@KeitelDOG
Copy link

KeitelDOG commented May 10, 2021

@mdvorscak thanks for putting me in the right path, now I think I've found the Perfect Solution for makeStyles + 'shallow+useContext`. While your solution was a good Starting point for me, it leads to some problem, and each problem solved leads to another:

  • it lost test coverage when calling useStyles which is now an empty function with no style
  • 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) like
    backgroundColor: props => {
      if (props.variant === 'contained') {
        return theme.palette[props.palette].main;
      }
      return 'unset';
    },
  • Lot of things broke when mocking useContext, because makeStyles result function uses useContext internally.

So I managed to solved all of these problems at the end. Here is how I manual mock makeStyles:

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

And 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.

@KeitelDOG
Copy link

And a version to mock it directly in test files instead of manual mock:

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

@KeitelDOG
Copy link

@oliviertassinari do you think that advance mockup might fit the snapshot use cases? I haven't try snapshot.

@oliviertassinari
Copy link
Member

oliviertassinari commented May 10, 2021

@KeitelDOG I would stay on this #15404 (comment), replace enzyme with react-testing-library as soon as you can.

@oliviertassinari oliviertassinari added test out of scope The problem looks valid but we won't fix it (maybe we will revisit it in the future) and removed duplicate This issue or pull request already exists labels May 10, 2021
@KeitelDOG
Copy link

@oliviertassinari I agree the react-testing-library is probably better to be used to avoid complications. And also I don't think others are asking to integrate the work around codes on material-ui. I do use mount for full rendering on some tests, but for most of them, we don't need to dive very deeply to render the same bottom level of components multiple times, especially when we're covering branch conditions. Using shallow to stay in a component is better when aiming at coverage.

I personally will try react-testing-library in some times from now, I'll have to set it up to see if it fits better my need, if it's relatively fast compared to Shallow. But I already see that the package is adding some level of complexity in terms of setup and syntaxes. But I hope it does worth it to let Shallow go for it.

@kombuchafox
Copy link

kombuchafox commented Sep 1, 2021

So this how I got custom Themes to work with Material UI. I used an augmented version @mdvorscak . We have a custom Theme sheet that I don't want have to override in every instance of the snapshot tests:

frontend/web/src/__mocks__/@material-ui/core.ts

// makeStyles applies THEME to makeStyles without having
// to use a context provider in snapshot tests.
const makeStyles = (args: any, options: any) => {
  // Use require so that we can lazily load the THEME.
  // If we don't, then there will be dependency cycle between
  // css/styles and @material-ui/core
  const THEME = require("css/styles");
  return Core.makeStyles(args, {
    defaultTheme: THEME,
    ...options,
  });
};

Note:
"css/styles" uses createMuiTheme to create new style

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
out of scope The problem looks valid but we won't fix it (maybe we will revisit it in the future) test
Projects
None yet
Development

No branches or pull requests

8 participants