Jest test fails : TypeError: window.matchMedia is not a function
Asked Answered
D

20

206

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.

Daune answered 3/10, 2016 at 11:31 Comment(0)
I
292

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

Incomprehensible answered 23/11, 2018 at 15:48 Comment(14)
This is the right answer. Note here that in your test you MUST import the mock BEFORE you import the file you are testing. eg ``` // import '../mockFile' // import '../fileToTest' ```Ghazi
Note that addListener and removeListener are deprecated, addEventListener and removeEventListener should be used instead. The full mock object can be found in the Jest docsFerree
How can the value be changed when mocking like this?Mcloughlin
@Mcloughlin see the answer I just posted, it might help you! (if you're still scratching your head since Feb 28!)Clasping
Where is that snippet supposed to go to solve the problem globally in a project?Irresolute
@Irresolute see the last code block in the docs linked in my answerIncomprehensible
The tests kept failing for me, until I used the solution offered here - #64813947Togo
Keeps failing for me when testing web with jest and enzyme :(Errand
I have an expo + next.js set up and keeps failing for web just to add some more context.Errand
NOTE: The only way this worked for me was to have the code in a separate file (NOT exported, just copy-paste as in the example) and do an import of the file. This example helped me: github.com/rickywid/projectx/blob/…Marlenemarler
github link to this bug fix issue: github.com/ant-design/ant-design/issues/21096Volplane
@Irresolute I would copy past in a jest-global-mocks.ts file.Ovid
hm, doesn't seem to work on window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change" ...Countdown
GitHub solution to "Cannot read property ‘matches’" It is a tweaked version of this answer where you directly redefine the window.matchMedia function.Chimere
C
78

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

Clean answered 9/3, 2017 at 3:7 Comment(5)
Where do you add this code? If I add it to the top of my testing file, then it still can’t find matchMedia.Implement
@HolgerEdwardWardlowSindbæk I edited my answer for more clarity!Clean
I got a TypeError: Cannot read property 'matches' of undefined exceptionDeliadelian
Adding following properties addListener: () and removeListener: () helps with avoiding additional fails with missing functions.Cheslie
Why setupFilesAfterEnv and not setupFiles?Coextensive
H
49

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() {}
    };
};
Heterogeneous answered 12/1, 2017 at 17:21 Comment(4)
and with in the test file, inside of 'describe' using jest, I write: global.window.matchMedia = jest.fn(() => { return { matches: false, addListener: jest.fn(), removeListener: jest.fn() } })Susurrate
How do you import the stub file?Implement
This works for one unit test, if you have multiple components having this same issue, you need to put this snippet into each test individually. Typically we want to avoid rewriting the same code, but if that's something that works for you, this is a great quick solution.Trichloroethylene
Isn't is addEventListener and removeEventListener?Grandson
T
29

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'],

Trichloroethylene answered 5/2, 2021 at 0:0 Comment(1)
This is the way.Overby
L
17

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

Lillis answered 4/10, 2016 at 15:50 Comment(0)
E
16

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.

Erg answered 27/7, 2021 at 4:46 Comment(3)
This was really useful for me, as all the other fixes extended the window object. If you're using Next.js and detecting server-side execution with typeof window === 'undefined' then those tests would in turn break.Chaperone
You save my dayBergquist
this is a fix for "TypeError: Cannot read properties of undefined (reading 'matches') at updateMatch" in React 18.Pinelli
S
13

You can mock the API:

describe("Test", () => {
  beforeAll(() => {
    Object.defineProperty(window, "matchMedia", {
      value: jest.fn(() => {
        return {
          matches: true,
          addListener: jest.fn(),
          removeListener: jest.fn()
        };
      })
    });
  });
});
Separable answered 8/5, 2019 at 16:57 Comment(2)
I particularly enjoy the simplicity and clarity of your approach, thanks for posting!Bypath
It works well for me even without this line matches: true,Arrant
G
12

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: () => {}
    };
  }
});
Galah answered 17/3, 2018 at 5:17 Comment(2)
Where do I add this? I tried adding to a setupFile but it doesnt workHittel
For me, ultimately, it's the file referred by "setupFile"Galah
T
11

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

Tusker answered 18/6, 2020 at 9:44 Comment(1)
most helpful answer so far...works like a charm, thanks! :)Managua
C
6

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

Clasping answered 25/10, 2020 at 5:17 Comment(2)
Out of all the solutions this is the only one that actually preserves the functionality of window.matchMedia, which is critical if your app's functionality / layout / etc depends on the media query (as do most reactive apps these days do). By mocking the matchMedia function in this way, you can dynamically set the window size and test the corresponding behaviour in your test suite. Many thanks @Clasping !Carpophagous
Resizing the window after mounting seems to do nothing. To see the updated window value in a rendered component I have to rerender it manually (using @testing-library/react).Mcloughlin
F
6

If you are using typescript, put the lines below in setupTests.ts file. This worked for me:

 export default global.matchMedia =
  global.matchMedia ||
  function (query) {
    return {
      matches: false,
      media: query,
      onchange: null,
      addListener: jest.fn(), // deprecated
      removeListener: jest.fn(), // deprecated
      addEventListener: jest.fn(),
      removeEventListener: jest.fn(),
      dispatchEvent: jest.fn(),
    };
  };
Foltz answered 5/10, 2022 at 11:29 Comment(0)
M
5

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.

Morra answered 7/3, 2021 at 10:22 Comment(0)
Z
3

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! :)

Zicarelli answered 5/10, 2018 at 4:14 Comment(0)
B
2

I developed a library specially designed for that: https://www.npmjs.com/package/mock-match-media

It proposes a complete implementation of matchMedia for node.

And it even has a jest-setup file you can import in your jest settings to apply this mock to all of your tests (see https://www.npmjs.com/package/mock-match-media#jest):

require('mock-match-media/jest-setup);
Brian answered 23/1, 2022 at 11:21 Comment(0)
B
2

You can also test if the type of window.matchMedia is a function before using it

Example :

if (typeof window.matchMedia === 'function') {
    // Do something with window.matchMedia
}

And the tests won't fails anymore

Broken answered 28/1, 2022 at 8:4 Comment(0)
E
2

The accepted answer used to work for me until recently when I updated my packages to the latest versions. I was getting an error Cannot read properties of undefined (reading 'matches') as for some reason the jest mock was not working as expected. So replacing the mock with a regular function worked for me:

Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: (query) => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(), // deprecated
    removeListener: jest.fn(), // deprecated
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn()
  })
});
Emblematize answered 2/8, 2023 at 4:23 Comment(1)
I don't get this, your polyfill is exactly the same?Saprophagous
B
1

These guys have a pretty slick solution via the Jest setupFiles:

https://github.com/HospitalRun/components/pull/117/commits/210d1b74e4c8c14e1ffd527042e3378bba064ed8

Enter image description here

Brooch answered 12/3, 2020 at 21:37 Comment(0)
N
1

Because I used a library that used window.matchMedia

what worked for me was requiring the Component (I use React) under test and the window.matchMedia mock inside jest.isolateModules()

function getMyComponentUnderTest(): typeof ComponentUnderTest {
  let Component: typeof ComponentUnderTest;

  // Must use isolateModules because we need to require a new module everytime so 
  jest.isolateModules(() => {
    // Required so the library (inside Component) won't fail as it uses the window.matchMedia
    // If we import/require it regularly once a new error will happen:
    // `TypeError: Cannot read property 'matches' of undefined`
    require('<your-path-to-the-mock>/__mocks__/window/match-media');
    
    Component = require('./<path-to-component>');
  });

  // @ts-ignore assert the Component (TS screams about using variable before initialization)
  // If for some reason in the future the behavior will change and this assertion will fail
  // We can do a workaround by returning a Promise and the `resolve` callback will be called with the Component in the `isolateModules` function
  // Or we can also put the whole test function inside the `isolateModules` (less preferred)
  expect(Component).toBeDefined();

  // @ts-ignore the Component must be defined as we assert it
  return Component;
}

window.matchMedia Mock (inside /__mocks__/window/match-media):

// Mock to solve: `TypeError: window.matchMedia is not a function`
// From https://mcmap.net/q/126635/-jest-test-fails-typeerror-window-matchmedia-is-not-a-function

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

// Making it a module so TypeScript won't scream about:
// TS1208: 'match-media.ts' cannot be compiled under '--isolatedModules' because it is considered a global script file. Add an import, export, or an empty 'export {}' statement to make it a module.
export {};
Nidanidaros answered 14/9, 2021 at 13:43 Comment(0)
G
1

If the component you are testing includes window.matchMedia() or imports another component (ie. a CSS media query hook uses useMedia() ) and you don't aim to test anything related to it, you can bypass calling the method by adding a window check to your component.

In the example code below, useMedia hook will always return false if the code runned by Jest.

There is a post about an argument against mocking module imports., https://dev.to/jackmellis/don-t-mock-modules-4jof

import { useLayoutEffect, useState } from 'react';

export function useMedia(query): boolean {
  const [state, setState] = useState(false);

  useLayoutEffect(() => {
    // ******* WINDOW CHECK START *******
    if (!window || !window.matchMedia) {
      return;
    }
    // ******* WINDOW CHECK END *******

    let mounted = true;
    const mql = window.matchMedia(query);
    const onChange = () => {
      if (!mounted) return;
      setState(!!mql.matches);
    };

    mql.addEventListener('change', onChange);
    setState(mql.matches);

    return () => {
      mounted = false;
      mql.removeEventListener('change', onChange);
    };
  }, [query]);

  return state;
}

But if you want to access the object returned from the method, you can mock it in the component itself, instead of testing files. see a sample usage: (source link)


import {useState, useEffect, useLayoutEffect} from 'react';
import {queryObjectToString, noop} from './utilities';
import {Effect, MediaQueryObject} from './types';

// ************** MOCK START **************
export const mockMediaQueryList: MediaQueryList = {
  media: '',
  matches: false,
  onchange: noop,
  addListener: noop,
  removeListener: noop,
  addEventListener: noop,
  removeEventListener: noop,
  dispatchEvent: (_: Event) => true,
};
// ************** MOCK END **************

const createUseMedia = (effect: Effect) => (
  rawQuery: string | MediaQueryObject,
  defaultState = false,
) => {
  const [state, setState] = useState(defaultState);
  const query = queryObjectToString(rawQuery);

  effect(() => {
    let mounted = true;
    
    ************** WINDOW CHECK START **************
    const mediaQueryList: MediaQueryList =
      typeof window === 'undefined'
        ? mockMediaQueryList
        : window.matchMedia(query);
    ************** WINDOW CHECK END **************
    const onChange = () => {
      if (!mounted) {
        return;
      }

      setState(Boolean(mediaQueryList.matches));
    };

    mediaQueryList.addListener(onChange);
    setState(mediaQueryList.matches);

    return () => {
      mounted = false;
      mediaQueryList.removeListener(onChange);
    };
  }, [query]);

  return state;
};

export const useMedia = createUseMedia(useEffect);
export const useMediaLayout = createUseMedia(useLayoutEffect);

export default useMedia;
Gratuity answered 10/10, 2021 at 11:18 Comment(0)
B
-3
describe('some test', () => {
window.matchMedia = window.matchMedia || function() {
    return {
        matches: false,
        addListener: function() {},
        removeListener: function() {}
    };
};

test('sdad', ()=> {
})
})
 
Badajoz answered 1/3, 2023 at 20:4 Comment(1)
You should probably explain how this answers the question and how this adds something the other answers have not.Calorific

© 2022 - 2024 — McMap. All rights reserved.