naemtl
naemtl

Reputation: 103

Trying to mock MUI's useMediaQuery hook behaviour in Enzyme test

EDIT: I was able to determine that MUI's instructions work correctly when using RTL. This issue is only taking place in Enzyme tests!

I'm following MUI's documentation on how to test useMediaQuery, but I am confused as to whether or not the way I am using useMediaQuery (outlined here in MUI's docs) in my component is compatible with the testing instructions in MUI's docs.

Here's the code in my component:

import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@material-ui/core/useMediaQuery';

  const List = () => {
      const theme = useTheme();
      const isDownLargeBreakpoint = 
      useMediaQuery(theme.breakpoints.down('lg'));

      ...
      
      {isDownLargeBreakpoint && (
          <ul className="list">
            // list items
          </ul>
      )}

  }

The useMediaQuery hook works as expected when I run my app locally, it correctly toggles between true and false when I resize the screen below/above MUI's theme lg breakpoint.

When I try to run my test with the recommended method of setup, despite the window.innerWidth falling below what would satisfy useMediaQuery to return a value of true I always get false in my test. Perhaps it's because I'm not rerendering my component from within my test? Or do I have to do something more in my it clause to trigger what is needing to happen?

Here's the block of code using css-mediaquery recommended by MUI as well as this post which was already answered:

import mediaQuery from 'css-mediaquery';

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

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

Here's how I've organized my test file:

import React from 'react';
import { shallow } from 'enzyme';
import mediaQuery from 'css-mediaquery';

import SessionStore from 'app/stores/SessionStore';

import CustomFields from '../CustomFields';
import CustomFieldButton from '../CustomFieldButton';

import PrepareFieldsList from '../PrepareFieldsList';

describe('PrepareFieldsList Component', () => {
  let wrapper;

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

  beforeAll(() => {
    window.matchMedia = createMatchMedia(window.innerWidth);
  });

  const defaultProps = {
    customFields: [
      {
        data: null,
        id: 'fieldId',
        name: '',
        required: false,
        value: 'test',
      },
    ],
  };

  beforeEach(() => {
    jest.spyOn(SessionStore, 'getSession').mockReturnValue({
      hasFeature: () => true,
    });
    wrapper = shallow(<PrepareFieldsList {...defaultProps} />);
  });

  ...

  it('should render CustomFieldButton and CustomFields when hasFeature is true', () => {
    expect(wrapper.find(CustomFieldButton)).toHaveLength(1);
    expect(wrapper.find(CustomFields)).toHaveLength(1);
  });
});

Upvotes: 1

Views: 1559

Answers (4)

A. Cosmin
A. Cosmin

Reputation: 1

I encountered a simillar issue while working with jsdom and vitest.

Here is what worked for me:

First I installed css-mediaquery

npm install [email protected] --save-dev

Then I installed @types/css-mediaquery (needed for typescript)

npm install @types/[email protected] --save-dev

Then implemented the createMatchMedia function and used as in the following example.

 //other imports
 import { match } from "css-mediaquery";

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

 describe.each([
               {
                 width: 1
               },
               {
                 width: 600
               } //etc, or create a util function to generate them
       ])(
        "Width: $width",
        ({ width }) => {
            beforeAll(() => {
                window.matchMedia = createMatchMedia(width);
            });

            it("should do something", () => {
                render(<MyComponent></MyComponent>, { wrapper: MyProviders}) //Make sure to add all necessary providers like the ThemeProvider with your theme as in <ThemeProvider theme={theme} ... etc

                //test here
                expect(screen.getByRole("button")).toBeInTheDocument();
            });
        }
    );

Upvotes: 0

trig
trig

Reputation: 1

I came across this after spending some time diagnosing my test, which was breaking because my custom breakpoints weren't being recognized. In the end, the answer for me was as simple as wrapping the component being tested in the MUI ThemeProvider (passing my theme). Otherwise, I used the example from the docs for implementing matchMedia (link).

<ThemeProvider theme={theme}>
  <Component />
<ThemeProvider>

Upvotes: 0

naemtl
naemtl

Reputation: 103

I figured it out. When doing this in Enzyme, you need to mock the hook and its return value with jest.

At the top of the test file:

import useMediaQuery from '@material-ui/core/useMediaQuery';

The mock at the top of the file, below the imports:

jest.mock('@material-ui/core/useMediaQuery');

Then to update the mock value:

useMediaQuery.mockReturnValueOnce(true);

Make sure to do this before you render your component in each test case. So like:

it('should render ChildComponent when useMediaQuery is true', () => {
    useMediaQuery.mockReturnValueOnce(true);
    const wrapper = shallow(<ParentComponent />);
    expect(wrapper).toContainExactlyOneMatchingElement(ChildComponent);
});

Upvotes: 1

Ali Sattarzadeh
Ali Sattarzadeh

Reputation: 3577

I think it's because innerWidth is not defined in window in jest and you need to set it's value manually, so you can pass value in createMatchMedia like : createMatchMedia(1000) or set value to window.innerWidth like this :

Object.defineProperty(window, 'innerWidth', {writable: true, configurable: true, value: 500})

and in your example would be sth like this :

 beforeAll(() => {
    Object.defineProperty(window, 'innerWidth', {writable: true, configurable: true, value: 500})
    window.matchMedia = createMatchMedia(window.innerWidth);
  });

Upvotes: 0

Related Questions