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

Created on 30 Jun 2019  ·  21Comments  ·  Source: enzymejs/enzyme

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 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 ( )
Need More Information

Most helpful comment

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

All 21 comments

I'm having the same issue. Any solutions?

What is comp.debug()?

What is comp.debug()?

That just prints out the HTML of the given comp for diagnostic purposes. I sed it to check if it was indeed picking up the correct component.

Also, in my case, the component works as intended (is rendered in correspondence with the breakpoint) when loaded in the browser,
but inside the testing environment (in my case Mocha with "jsdom-global/register" installed with npm)
it is not rendered despite checking the window.innerWidth (which when logged is 1024 as in @asnaseer-resilient case)

Just found the problem in my case!
In my component i was using the prop mdDown which is true when viewport width is BELOW 1280px
and i thought it was true only when below 960px!!!

@asnaseer-resilient that doesn't actually print out the HTML, that prints out the enzyme react tree, which contains non-html components - can you please provide the actual full output of comp.debug()?

@asnaseer-resilient that doesn't actually print out the HTML, that prints out the enzyme react tree, which contains non-html components - can you please provide the actual full output of comp.debug()?

I have already provided the full output in my report above - repeated here:

  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>

I guess I'm a bit confused, because you're using mount, and Hidden outputs WithWidth(HiddenJs) but no other children. What are these components?

Also, your test is looking for an ID of "'im-also-here", but you can clearly see that it is not present in that debug output. Why would you expect hiddenComp.exists() to be truthy, when it doesn't actually exist?

I guess I'm a bit confused, because you're using mount, and Hidden outputs WithWidth(HiddenJs) but no other children. What are these components?

Also, your test is looking for an ID of "'im-also-here", but you can clearly see that it is _not_ present in that debug output. Why would you expect hiddenComp.exists() to be truthy, when it doesn't actually exist?

Yes - that is the point. The hiddenComp _should_ be present as the test is being run with a Window width of 1024 (as displayed in the console output). So both comp.debug() and the test itself fail to "see" this component.

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

It's a fairly simple setup so you should be able to reproduce this in your environment which might help in diagnosing why this is failing.

I believe jsdom has issues with correctly reporting viewport sizes. Can you share the exact component code that responds to the window width?

I believe jsdom has issues with correctly reporting viewport sizes. Can you share the exact component code that responds to the window width?

I have included that code in the report as follows:

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;

what about Hidden? are you relying on App.css to do anything?

Hidden is a standard component from the @material-ui library. App.css is exactly what the default create-react-app generates and nothing inside it is being used in the example.

This is the contents of App.css:

.App {
  text-align: center;
}

.App-logo {
  animation: App-logo-spin infinite 20s linear;
  height: 40vmin;
  pointer-events: none;
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

Hi, I have the same problem.

When I want to test hidden nested components, I don't find any nested elements.
The only way to see this elements, it's to change the implementation property to css

But I want to keep the JS implementation.

Please give me some help with this Hidden component 🙏

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 https://github.com/testing-library/jest-dom/issues/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: https://github.com/jsdom/jsdom/issues/2342#issuecomment-415086243 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.

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.

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

@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 <3!

Thanks for confirming!!

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')
        })
Was this page helpful?
0 / 5 - 0 ratings

Related issues

timhonders picture timhonders  ·  3Comments

nelsonchen90 picture nelsonchen90  ·  3Comments

ahuth picture ahuth  ·  3Comments

ivanbtrujillo picture ivanbtrujillo  ·  3Comments

heikkimu picture heikkimu  ·  3Comments