'Jest + Material-UI : Correctly mocking useMediaQuery

I'm using Material-UI's useMediaQuery() function in one of my components to determine the size prop to use for a <Button> within the component.

I'm trying to test that it's working as expected in a jest test, however my current implementation isn't working:

describe("Unit: <Navbar> On xs screens", () => {

  // Incorrectly returns `matches` as `false` ****************************
  window.matchMedia = jest.fn().mockImplementation(
    query => {
      return {
        matches: true,
        media: query,
        onchange: null,
        addListener: jest.fn(),
        removeListener: jest.fn()
      };
    }
  );

  it("renders as snapshot", async () => {
    const width = theme.breakpoints.values.sm - 1;
    const height = Math.round((width * 9) / 16);
    Object.defineProperty(window, "innerWidth", {
      writable: true,
      configurable: true,
      value: width
    });
    const { asFragment } = render(
      <Container backgroundColor={"#ffffff"}>
        <Navbar />
      </Container>
    );
    expect(asFragment()).toMatchSnapshot();
    const screenshot = await generateImage({
      viewport: { width, height }
    });
    expect(screenshot).toMatchImageSnapshot();
  });
});

describe("Unit: <Navbar> On md and up screens", () => {

  // Correctly returns `matches` as `false` ****************************
  window.matchMedia = jest.fn().mockImplementation(
    query => {
      return {
        matches: false,
        media: query,
        onchange: null,
        addListener: jest.fn(),
        removeListener: jest.fn()
      };
    }
  );

  it("renders as snapshot", async () => {
    const width = theme.breakpoints.values.md;
    const height = Math.round((width * 9) / 16);
    Object.defineProperty(window, "innerWidth", {
      writable: true,
      configurable: true,
      value: width
    });
    const { asFragment } = render(
      <Container backgroundColor={"#ffffff"}>
        <Navbar />
      </Container>
    );
    expect(asFragment()).toMatchSnapshot();
    const screenshot = await generateImage({
      viewport: { width, height }
    });
    expect(screenshot).toMatchImageSnapshot();
  });
});

And the component I'm testing (removed irrelevant parts):

const Navbar = () => {
  const theme = useTheme();
  const matchXs = useMediaQuery(theme.breakpoints.down("xs"));
  return (
    <Button size={matchXs ? "medium" : "large"}>
      Login
    </Button>
  );
};
export default Navbar;

It's returning matches as false for the first test, even though I've set it to return as true. I know this because it's generating a screenshot and I can see that the button size is set to large for the first test when it should be set to medium.

It works as expected in production.

How do I correctly get mock useMediaQuery() in a jest test?



Solution 1:[1]

The recommended way is using css-mediaquery which is now mentioned in the MUI docs:

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

Solution 2:[2]

I figured it out...

useMediaQuery() needs to re-render the component to work, as the first render will return whatever you define in options.defaultMatches (false by default).

Also, the mock needs to be scoped to each test (it), not in the describe.

As I'm using react-testing-library, all I have to do is re-render the component again and change the scope of the mock and it works.

Here's the working example:

const initTest = width => {
  Object.defineProperty(window, "innerWidth", {
    writable: true,
    configurable: true,
    value: width
  });
  window.matchMedia = jest.fn().mockImplementation(
    query => {
      return {
        matches: width >= theme.breakpoints.values.sm ? true : false,
        media: query,
        onchange: null,
        addListener: jest.fn(),
        removeListener: jest.fn()
      };
    }
  );
  const height = Math.round((width * 9) / 16);
  return { width, height };
};

describe("Unit: <Navbar> On xs screens", () => {
  it("renders as snapshot", async () => {
    const { width, height } = initTest(theme.breakpoints.values.sm - 1);
    const { asFragment, rerender} = render(
      <Container backgroundColor={"#ffffff"}>
        <Navbar />
      </Container>
    );
    rerender(
      <Container backgroundColor={"#ffffff"}>
        <Navbar />
      </Container>
    );
    expect(asFragment()).toMatchSnapshot();
    const screenshot = await generateImage({
      viewport: { width, height }
    });
    expect(screenshot).toMatchImageSnapshot();
  });
});

describe("Unit: <Navbar> On md and up screens", () => {
  it("renders as snapshot", async () => {
    const { width, height } = initTest(theme.breakpoints.values.md);
    const { asFragment } = render(
      <Container backgroundColor={"#ffffff"}>
        <Navbar />
      </Container>
    );
    rerender(
      <Container backgroundColor={"#ffffff"}>
        <Navbar />
      </Container>
    );
    expect(asFragment()).toMatchSnapshot();
    const screenshot = await generateImage({
      viewport: { width, height }
    });
    expect(screenshot).toMatchImageSnapshot();
  });
});

Solution 3:[3]

A simple approach that worked for me:

component.tsx

import { Fade, Grid, useMediaQuery } from '@material-ui/core';

...
const isMobile = useMediaQuery('(max-width:600px)');

component.spec.tsx

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

// not testing for mobile as default
jest.mock('@material-ui/core', () => ({
  ...jest.requireActual('@material-ui/core'),
  useMediaQuery: jest.fn().mockReturnValue(false),
}));

describe('...', () => {
it(...)

// test case for mobile
it('should render something for mobile', () => {
  ((useMediaQuery as unknown) as jest.Mock).mockReturnValue(true);
  ....
})

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Alexandre N.
Solution 3 AndyFaizan