Jake
Jake

Reputation: 4255

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?

Upvotes: 10

Views: 15943

Answers (5)

manish
manish

Reputation: 1

Maybe we can do something like this:

const applyMock = (mobile) => {
  window.matchMedia = jest.fn().mockImplementation((query) => {
    return {
      matches: mobile,
      media: query,
      addListener: jest.fn(),
      removeListener: jest.fn(),
    };
  });
};

and use it like this:

const mockMediaQueryForMobile = () => applyMock(true);
const mockMediaQueryForDesktop = () => applyMock(false);

Upvotes: 0

Hans
Hans

Reputation: 1572

Inspired by @iman-mahmoudinasab's accepted answer, here is a TypeScript implementation. Essentially, we would want to create valid MediaQueryList-object:

// yarn add -D css-mediaquery @types/css-mediaquery
import mediaQuery from 'css-mediaquery';

describe('Foo Bar', () => {
  beforeAll(() => {
    function createMatchMedia(width: number) {
      return (query: string): MediaQueryList => ({
        matches: mediaQuery.match(query, { width }) as boolean,
        media: '',
        addListener: () => {},
        removeListener: () => {},
        onchange: () => {},
        addEventListener: () => {},
        removeEventListener: () => {},
        dispatchEvent: () => true,
      });
    }
    // mock matchMedia for useMediaQuery to work properly
    window.matchMedia = createMatchMedia(window.innerWidth);
  });
});

Upvotes: 3

AndyFaizan
AndyFaizan

Reputation: 1893

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

Upvotes: 5

Jake
Jake

Reputation: 4255

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

Upvotes: 3

Iman Mahmoudinasab
Iman Mahmoudinasab

Reputation: 7015

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

Upvotes: 17

Related Questions