108
votes

This is my first front-end testing experience. In this project, I'm using Jest snapshot testing and got an error TypeError: window.matchMedia is not a function inside my component.

I go through Jest documentation, I found the "Manual mocks" section, but I have not any idea about how to do that yet.

13

13 Answers

172
votes

The Jest documentation now has an "official" workaround:

Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(), // Deprecated
    removeListener: jest.fn(), // Deprecated
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});

Mocking methods which are not implemented in JSDOM

45
votes

I've been using this technique to solve a bunch of mocking problems.

describe("Test", () => {
  beforeAll(() => {
    Object.defineProperty(window, "matchMedia", {
      writable: true,
      value: jest.fn().mockImplementation(query => ({
        matches: false,
        media: query,
        onchange: null,
        addListener: jest.fn(), // Deprecated
        removeListener: jest.fn(), // Deprecated
        addEventListener: jest.fn(),
        removeEventListener: jest.fn(),
        dispatchEvent: jest.fn(),
      }))
    });
  });
});

Or, if you want to mock it all the time, you could put inside your mocks file called from your package.json: "setupFilesAfterEnv": "<rootDir>/src/tests/mocks.js",.

Reference: setupTestFrameworkScriptFile

26
votes

I put a matchMedia stub in my Jest test file (above the tests), which allows the tests to pass:

window.matchMedia = window.matchMedia || function() {
    return {
        matches: false,
        addListener: function() {},
        removeListener: function() {}
    };
};
15
votes

Jest uses jsdom to create a browser environment. JSDom doesn't however support window.matchMedia so you will have to create it yourself.

Jest's manual mocks work with module boundaries, i.e. require / import statements so they wouldn't be appropriate to mock window.matchMedia as is because it's a global.

You therefore have two options:

  1. Define your own local matchMedia module which exports window.matchMedia. -- This would allow you to then define a manual mock to use in your test.

  2. Define a setup file which adds a mock for matchMedia to the global window.

With either of these options you could use a matchMedia polyfill as a mock which would at least allow your tests to run or if you needed to simulate different states you might want to write your own with private methods allowing you to configure it's behaviour similar to the Jest fs manual mock

10
votes

I have just encountered this issue and had to mock these in jestGlobalMocks.ts:

Object.defineProperty(window, 'matchMedia', {
  value: () => {
    return {
      matches: false,
      addListener: () => {},
      removeListener: () => {}
    };
  }
});

Object.defineProperty(window, 'getComputedStyle', {
  value: () => {
    return {
      getPropertyValue: () => {}
    };
  }
});
8
votes

You can use the jest-matchmedia-mock package for testing any media queries (like device screen change, color-scheme change, etc.)

7
votes

You can mock the API:

describe("Test", () => {
  beforeAll(() => {
    Object.defineProperty(window, "matchMedia", {
      value: jest.fn(() => {
        return {
          matches: true,
          addListener: jest.fn(),
          removeListener: jest.fn()
        };
      })
    });
  });
});
3
votes

I tried all the above previous answers without any success.

Adding matchMedia.js to the mocks folder, did it for me.

I filled it up with techguy2000's content:

// __mocks__/matchMedia.js
'use strict';

Object.defineProperty(window, 'matchMedia', {
    value: () => ({
        matches: false,
        addListener: () => {},
        removeListener: () => {}
    })
});

Object.defineProperty(window, 'getComputedStyle', {
    value: () => ({
        getPropertyValue: () => {}
    })
});

module.exports = window;

And then imported this in setup.js:

import matchMedia from '../__mocks__/matchMedia';

Boom! :)

3
votes

JESTS OFFICIAL WORKAROUND

is to create a mock file, called matchMedia.js and add the following code:

Object.defineProperty(window, 'matchMedia', {
    writable: true,
    value: jest.fn().mockImplementation((query) => ({
        matches: false,
        media: query,
        onchange: null,
        addListener: jest.fn(), // Deprecated
        removeListener: jest.fn(), // Deprecated
        addEventListener: jest.fn(),
        removeEventListener: jest.fn(),
        dispatchEvent: jest.fn(),
    })),
});

Then, inside your test file, import your mock import './matchMedia'; and as long as you import it in every use case, it should solve your problem.

ALTERNATIVE OPTION

I kept running into this issue and found myself just making too many imports, thought I would offer an alternative solution.

which is to create a setup/before.js file, with the following contents:

import 'regenerator-runtime';

/** Add any global mocks needed for the test suite here */

Object.defineProperty(window, 'matchMedia', {
    writable: true,
    value: jest.fn().mockImplementation((query) => ({
        matches: false,
        media: query,
        onchange: null,
        addListener: jest.fn(), // Deprecated
        removeListener: jest.fn(), // Deprecated
        addEventListener: jest.fn(),
        removeEventListener: jest.fn(),
        dispatchEvent: jest.fn(),
    })),
});

And then inside your jest.config file, add the following:

setupFiles: ['<rootDir>/ROUTE TO YOUR BEFORE.JS FILE'],

3
votes

The official workaround worked for me until I decided to update react-scripts from 3.4.1 to 4.0.3 (as I use create-react-app). Then I started getting an error Cannot read property 'matches' of undefined.

So here's workaround I found. Install mq-polyfill as dev dependency.

Then code this in src/setupTests.js:

import matchMediaPolyfill from 'mq-polyfill'

matchMediaPolyfill(window)

// implementation of window.resizeTo for dispatching event
window.resizeTo = function resizeTo(width, height) {
  Object.assign(this, {
    innerWidth: width,
    innerHeight: height,
    outerWidth: width,
    outerHeight: height
  }).dispatchEvent(new this.Event('resize'))
}

This worked for me.

2
votes

TL;DR answer further down below

In my case, the answer was not enough, as window.matchMedia would always return false (or true if you change it). I had some React hooks and components that needed to listen to multiple different queries with possibly different matches.

What I tried

If you only need to test one query at a time and your tests don't rely on multiple matches, jest-matchmedia-mock was useful. However, from what I've understood after trying to use it for 3 hours was that when you call useMediaQuery, the previous queries you've made no longer work. In fact, the query you pass into useMediaQuery will just match true whenever your code calls window.matchMedia with that same query, regardless of the actual "window width".

Answer

After realizing I couldn't actually test my queries with jest-matchmedia-mock, I changed the original answer a bit to be able to mock the behavior of dynamic query matches. This solution requires the css-mediaquery npm package.

import mediaQuery from "css-mediaquery";

// Mock window.matchMedia's impl.
Object.defineProperty(window, "matchMedia", {
    writable: true,
    value: jest.fn().mockImplementation((query) => {
        const instance = {
            matches: mediaQuery.match(query, {
                width: window.innerWidth,
                height: window.innerHeight,
            }),
            media: query,
            onchange: null,
            addListener: jest.fn(), // Deprecated
            removeListener: jest.fn(), // Deprecated
            addEventListener: jest.fn(),
            removeEventListener: jest.fn(),
            dispatchEvent: jest.fn(),
        };

        // Listen to resize events from window.resizeTo and update the instance's match
        window.addEventListener("resize", () => {
            const change = mediaQuery.match(query, {
                width: window.innerWidth,
                height: window.innerHeight,
            });

            if (change != instance.matches) {
                instance.matches = change;
                instance.dispatchEvent("change");
            }
        });

        return instance;
    }),
});

// Mock window.resizeTo's impl.
Object.defineProperty(window, "resizeTo", {
    value: (width: number, height: number) => {
        Object.defineProperty(window, "innerWidth", {
            configurable: true,
            writable: true,
            value: width,
        });
        Object.defineProperty(window, "outerWidth", {
            configurable: true,
            writable: true,
            value: width,
        });
        Object.defineProperty(window, "innerHeight", {
            configurable: true,
            writable: true,
            value: height,
        });
        Object.defineProperty(window, "outerHeight", {
            configurable: true,
            writable: true,
            value: height,
        });
        window.dispatchEvent(new Event("resize"));
    },
});

It uses css-mediaquery with the window.innerWidth to determine if the query ACTUALLY matches instead of a hard-coded boolean. It also listens to resize events fired by the window.resizeTo mocked implementation to update the matches value.

You may now use window.resizeTo in your tests to change the window's width so your calls to window.matchMedia reflect this width. Here's an example, which was made just for this question, so ignore the performance issues it has!

const bp = { xs: 200, sm: 620, md: 980, lg: 1280, xl: 1920 };

// Component.tsx
const Component = () => {
  const isXs = window.matchMedia(`(min-width: ${bp.xs}px)`).matches;
  const isSm = window.matchMedia(`(min-width: ${bp.sm}px)`).matches;
  const isMd = window.matchMedia(`(min-width: ${bp.md}px)`).matches;
  const isLg = window.matchMedia(`(min-width: ${bp.lg}px)`).matches;
  const isXl = window.matchMedia(`(min-width: ${bp.xl}px)`).matches;

  console.log("matches", { isXs, isSm, isMd, isLg, isXl });

  const width =
    (isXl && "1000px") ||
    (isLg && "800px") ||
    (isMd && "600px") ||
    (isSm && "500px") ||
    (isXs && "300px") ||
    "100px";

  return <div style={{ width }} />;
};

// Component.test.tsx
it("should use the md width value", () => {
  window.resizeTo(bp.md, 1000);

  const wrapper = mount(<Component />);
  const div = wrapper.find("div").first();

  // console.log: matches { isXs: true, isSm: true, isMd: true, isLg: false, isXl: false }

  expect(div.prop("style")).toHaveProperty("width", "600px");
});

Note: I have not tested the behavior of this when resizing the window AFTER mounting the component

0
votes

Add following lines to your setupTest.js file,

global.matchMedia = global.matchMedia || function() {
    return {
        matches : false,
        addListener : function() {},
        removeListener: function() {}
    }
}

This would add match media query for your all test cases.