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

Unable to test React with Material-UI Hidden element #2179

Closed
11 tasks
asnaseer-resilient opened this issue Jun 30, 2019 · 27 comments
Closed
11 tasks

Unable to test React with Material-UI Hidden element #2179

asnaseer-resilient opened this issue Jun 30, 2019 · 27 comments

Comments

@asnaseer-resilient
Copy link

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 ( )
@vassilispanagou
Copy link

I'm having the same issue. Any solutions?

@ljharb
Copy link
Member

ljharb commented Sep 2, 2019

What is comp.debug()?

@asnaseer-resilient
Copy link
Author

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.

@vassilispanagou

This comment has been minimized.

@vassilispanagou

This comment has been minimized.

@ljharb
Copy link
Member

ljharb commented Sep 3, 2019

@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
Copy link
Author

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

@ljharb
Copy link
Member

ljharb commented Sep 4, 2019

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?

@asnaseer-resilient
Copy link
Author

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.

@ljharb
Copy link
Member

ljharb commented Sep 4, 2019

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

@asnaseer-resilient
Copy link
Author

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;

@ljharb
Copy link
Member

ljharb commented Sep 5, 2019

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

@asnaseer-resilient
Copy link
Author

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.

@asnaseer-resilient
Copy link
Author

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

@nicolasremise
Copy link

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 🙏

@ljharb
Copy link
Member

ljharb commented Sep 6, 2019

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.

@ljharb ljharb closed this as completed Sep 6, 2019
@asnaseer-resilient
Copy link
Author

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.

@nicolasremise
Copy link

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

@oliviertassinari
Copy link

oliviertassinari commented Sep 9, 2019

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

@ljharb
Copy link
Member

ljharb commented Sep 9, 2019

Thanks for confirming!!

@rmakarov
Copy link

rmakarov commented Aug 27, 2020

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

@RobertaMelo
Copy link

RobertaMelo commented Jun 16, 2021

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

@japser36
Copy link

japser36 commented Oct 8, 2021

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.

@genepaul
Copy link

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

@jonatasoc
Copy link

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

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

@imnasnainaec
Copy link

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

@jcruzv-prog
Copy link

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

Did you find any solution to do the test with the sx prop? Thanks in advance

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests