enzyme: Unable to test React with Material-UI Hidden element

I have a managed to reproduce my issue in a very simple React app that I created via npx create-react-app xxx. I then installed material-ui/core, enzyme, and enzyme-adapter-react-16 resulting in this package.json:

{
  "name": "xxx",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@material-ui/core": "^4.1.3",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-scripts": "3.0.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "enzyme": "^3.10.0",
    "enzyme-adapter-react-16": "^1.14.0"
  }
}

I then modified the default App.js to this:

import React from 'react';
import Hidden from '@material-ui/core/Hidden';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <div id='im-here'>Hello World!</div>
        <Hidden xsDown><div id='im-also-here'>Goodbye World!</div></Hidden>
      </header>
    </div>
  );
}

export default App;

and modified the default App.test.js to this:

import React from 'react';
import Enzyme, { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

import App from './App';

Enzyme.configure({adapter: new Adapter()});

describe('test', () => {
  it('should work', () => {
    console.log(window.innerWidth, window.innerHeight);
    const comp = mount(<App />);
    console.log(comp.debug());
    const nonHiddenComp = comp.find('#im-here');
    expect(nonHiddenComp.exists()).toBeTruthy();
    const hiddenComp = comp.find('#im-also-here');
    expect(hiddenComp.exists()).toBeTruthy();
  });
});

Current behavior

When I run the tests using npm test I get this output:

 FAIL  src/App.test.js
  test
    ✕ should work (41ms)

  ● test › should work

    expect(received).toBeTruthy()

    Received: false

      15 |     expect(nonHiddenComp.exists()).toBeTruthy();
      16 |     const hiddenComp = comp.find('#im-also-here');
    > 17 |     expect(hiddenComp.exists()).toBeTruthy();
         |                                 ^
      18 |   });
      19 | });
      20 | 

      at Object.toBeTruthy (src/App.test.js:17:33)

  console.log src/App.test.js:11
    1024 768

  console.log src/App.test.js:13
    <App>
      <div className="App">
        <header className="App-header">
          <div id="im-here">
            Hello World!
          </div>
          <Hidden xsDown={true} implementation="js" lgDown={false} lgUp={false} mdDown={false} mdUp={false} smDown={false} smUp={false} xlDown={false} xlUp={false} xsUp={false}>
            <WithWidth(HiddenJs) xsDown={true} lgDown={false} lgUp={false} mdDown={false} mdUp={false} smDown={false} smUp={false} xlDown={false} xlUp={false} xsUp={false} />
          </Hidden>
        </header>
      </div>
    </App>

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        2.511s
Ran all test suites related to changed files.

Expected behavior

If I run npm start then I correctly see both Hello World! and Goodbye World! elements.

However, from the output above you can see that the test fails to find the element protected by the <Hidden> material-ui element. The output shows that the test is running with a window of size 1024px by 768px. I would therefore have expected this element to have been visible and therefore found by the above test.

Your environment

Mac OS Version 10.14.5 (18F132)

API

  • shallow
  • [X ] mount
  • render

Version

library version
enzyme 3.10.0
react 16.8.6
react-dom 16.8.6
react-test-renderer 16.8.6
@material-ui/core 4.1.3
adapter (below)

Adapter

  • [X ] enzyme-adapter-react-16
  • enzyme-adapter-react-16.3
  • enzyme-adapter-react-16.2
  • enzyme-adapter-react-16.1
  • enzyme-adapter-react-15
  • enzyme-adapter-react-15.4
  • enzyme-adapter-react-14
  • enzyme-adapter-react-13
  • enzyme-adapter-react-helper
  • others ( )

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 5
  • Comments: 27 (7 by maintainers)

Most upvoted comments

I found a trick after some investigation.

You can hack materialUI by passing a custom theme to your mount. If you look the withWidth.js file you can see that the WithWidth method extract some properties from the current theme.

And if you read comments about initialWidth in PropTypes definition, you understand that initialWidth allows you to define a basic width, even if you are in jsdom env 😉

So, for me, I use it like this and my mounted component generate my Hidden children correctly.

const theme = createMuiTheme({ props: { MuiWithWidth: { initialWidth: 'xs' } } })
const wrapper = createMount()(<MuiThemeProvider theme={theme}>
        <MyComponentWithHidden />
    </MuiThemeProvider>);

OK, so the implication is that something about Material UI’s Hidden component’s JS implementation conflicts with jsdom, or, uses hooks in a way that doesn’t work with mount (which would be a surprising and new problem, and one that definitely needs fixing).

That leads me here: https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/Hidden/HiddenJs.js

It’s using useTheme, but this seems unrelated; withWidth() seems to be relevant, which leads me here: https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/withWidth/withWidth.js

This seems to be using media queries. https://github.com/jsdom/jsdom/blob/d6f8a97b5fb7709d6ad0215c1ae95fd4cab58489/lib/jsdom/level2/style.js#L29 and testing-library/jest-dom#113 both strongly suggest that jsdom does not support media queries - which means that it’s impossible to test this implementation outside of a real browser (a suggestion like this: jsdom/jsdom#2342 (comment) which has you manually set the width may work, but probably won’t work with media queries)

Thus, I’m going to close this since it’s either a bug in jsdom, or a flaw in Material UI’s choice of implementation.

I’d suggest using shallow rendering anyways - testing Material UI itself is not something you really need to care about, since that’s what its own tests cover.

Thanks for the investigation - much appreciated.

In response to your last comment - I am not actually testing MaterialUI, what I am testing is that the developer has coded to spec, i.e. certain elements should be visible on a page when on a desktop browser but not when on a mobile device.

I will raise this as an issue on both the MaterialUI and jsdom github repos to see if either of them can shed any further light on how to fix this.

@ljharb You are right. We encourage the usage of a media query polyfill with jsdom in the documentation https://material-ui.com/components/use-media-query/#testing. And thanks for looking at it ❤️!

@ljharb You are right. We encourage the usage of a media query polyfill with jsdom in the documentation https://material-ui.com/components/use-media-query/#testing. And thanks for looking at it ❤️!

That solution in the MUI docs worked for me:

import mediaQuery from 'css-mediaquery';

function createMatchMedia(width) {
  return query => ({
    matches: mediaQuery.match(query, { width }),
    addListener: () => {},
    removeListener: () => {},
  });
}

describe('MeusTestes', () => {
  beforeAll(() => {
    window.matchMedia = createMatchMedia(window.innerWidth);
  });
});

I found a trick after some investigation.

You can hack materialUI by passing a custom theme to your mount. If you look the withWidth.js file you can see that the WithWidth method extract some properties from the current theme.

And if you read comments about initialWidth in PropTypes definition, you understand that initialWidth allows you to define a basic width, even if you are in jsdom env 😉

So, for me, I use it like this and my mounted component generate my Hidden children correctly.

const theme = createMuiTheme({ props: { MuiWithWidth: { initialWidth: 'xs' } } })
const wrapper = createMount()(<MuiThemeProvider theme={theme}>
        <MyComponentWithHidden />
    </MuiThemeProvider>);

It seems that as of MUI v5, this solution (which worked great for us for a while! thanks!) no longer works unfortunately, as it seems they have entirely removed WithWidth, so it’s no longer possible to specify it with this props solution during theme creation. They did change their theming structure but there is no equivalent alternative in this new format to specify anything for withWidth in this way, since withWidth is gone. In our code base we had to switch a lot of components to using css media queries in their classes, since those do respond to global.innerWidth = ... calls in test rendering, at least for our class components. For our functional components we could follow this part of the migration guide to switch to the useMediaQuery hook, at least for our Hidden components.

My solution to test content - wrapped in Hidden element:

 it('should display correct score of the selected question while landing on question player', () => {
            const topPanelScoreHiddenElement = questionPlayer.find('div[data-testid="assessment-top-panel-score"]').find(Hidden).at(0)
            const topPanelScore = mount(<>{topPanelScoreHiddenElement.prop('children')}</>)

            expect(topPanelScore.find('h4').text())
                .toEqual('2 / 12')
        })

For anybody using Typescript:

function createMatchMedia(width: number): (query: string) => MediaQueryList {
  return (query: string) =>
    ({
      matches: mediaQuery.match(query, { width }),
      addListener: () => {},
      removeListener: () => {},
    }) as any;
}

or

function createMatchMedia(width: number): (query: string) => MediaQueryList {
  return (query: string) => ({
    matches: mediaQuery.match(query, { width }),
    media: "",
    addEventListener: () => {},
    addListener: () => {},
    dispatchEvent: () => false,
    onchange: () => {},
    removeEventListener: () => {},
    removeListener: () => {},
  });
}

Or with jest:

function createMatchMedia(width: number): (query: string) => MediaQueryList {
  return (query: string) =>
    ({
      matches: mediaQuery.match(query, { width }),
      addListener: jest.fn(),
      removeListener: jest.fn(),
    }) as any;
};

@japser36, do you have an example of the changes you made? I am struggling with this same issue, and changing global.innerWidth doesn’t seem to be affecting when I use useMediaQuery, or if I use the sx display prop.

My solution to test content - wrapped in Hidden element:

 it('should display correct score of the selected question while landing on question player', () => {
            const topPanelScoreHiddenElement = questionPlayer.find('div[data-testid="assessment-top-panel-score"]').find(Hidden).at(0)
            const topPanelScore = mount(<>{topPanelScoreHiddenElement.prop('children')}</>)

            expect(topPanelScore.find('h4').text())
                .toEqual('2 / 12')
        })

Your solution work for me, thank you! I just put:

const showHiddenElement = listComponent .find(‘li[id=“listWithBreakline”]’) .find(‘Hidden’) .at(0); const mountedHiddenElement = mount( <>{showHiddenElement.prop(‘children’)}</> ); expect(mountedHiddenElement).toMatchSnapshot();

And we can see the element in the snapshot! We can put the id on the Hidden element and find by him:

const showHiddenElement = listComponent.find(‘Hidden’).find(‘#xsUP’).at(0); const mountedHiddenElement = mount( <>{showHiddenElement.prop(‘children’)}</> ); expect(mountedHiddenElement).toMatchSnapshot();