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

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

这是我的第一次前端测试经历。在这个项目中,我正在使用 Jest 快照测试并在我的组件中出现错误 TypeError: window.matchMedia is not a function

我浏览了 Jest 文档,找到了“手动模拟”部分,但我还不知道该怎么做。

Jest 使用 jsdom 创建浏览器环境。但是 JSDom 不支持 window.matchMedia 所以你必须自己创建它。

Jest 的 manual mocks 使用模块边界,即 require / import 语句,因此它们不适合模拟 window.matchMedia 因为它是全局的。

因此您有两个选择:

  1. 定义您自己的导出 window.matchMedia 的本地 matchMedia 模块。 -- 这将允许您定义一个手动模拟以在您的测试中使用。

  2. 定义一个 setup file,它将 matchMedia 的模拟添加到全局 window。

使用这些选项中的任何一个,您都可以使用 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

我在我的 Jest 测试文件中放置了一个 matchMedia 存根(在测试之上),它允许测试通过:

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

我一直在使用这种技术来解决一堆模拟问题。

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

或者,如果您想一直模拟它,您可以将 mocks 文件放入从 package.json 调用的文件中: "setupFilesAfterEnv": "<rootDir>/src/tests/mocks.js",.

参考:setupTestFrameworkScriptFile

我刚遇到这个问题,不得不在 jestGlobalMocks.ts 中模拟这些:

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

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

我尝试了以上所有答案,但都没有成功。

将 matchMedia.js 添加到 mocks 文件夹,帮我完成了。

我填了 :

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

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

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

module.exports = window;

然后在 setup.js 中导入:

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

轰! :)

Jest 文档现在有一个“官方”解决方法:

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

你可以模拟 API:

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

这些人通过 Jest setupFiles 有一个非常巧妙的解决方案:

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

您可以使用 jest-matchmedia-mock 包来测试任何媒体查询(例如设备屏幕更改、配色方案更改等)

TL;DR 在下面进一步回答

在我的例子中,答案是不够的,因为 window.matchMedia 总是 return false(或者 true 如果你改变它)。我有一些 React 挂钩和组件需要监听 多个不同的 查询,这些查询可能有不同的 matches.

我试过的

如果您一次只需要测试一个查询并且您的测试不依赖于多个匹配项,jest-matchmedia-mock 很有用。但是,根据我在尝试使用它 3 小时后的理解,当您调用 useMediaQuery 时,您之前所做的查询不再有效。事实上,只要您的代码使用相同的查询调用 window.matchMedia,您传递给 useMediaQuery 的查询将只匹配 true,而不管实际的“window 宽度”。

回答

在意识到我无法使用 jest-matchmedia-mock 实际测试我的查询后,我稍微更改了原始答案,以便能够模拟动态查询 matches 的行为。此解决方案需要 css-mediaquery npm 包。

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

它使用 css-mediaquerywindow.innerWidth 来确定查询 ACTUALLY 是否匹配而不是 hard-coded 布尔值。它还侦听由 window.resizeTo 模拟实现触发的调整大小事件以更新 matches 值。

您现在可以在测试中使用 window.resizeTo 来更改 window 的宽度,以便您对 window.matchMedia 的调用反映此宽度。这是一个例子,就是为了这个问题而做的,所以请忽略它的性能问题!

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

注意:在安装组件

后调整window大小时,我还没有测试这个行为

开玩笑OFFICIAL WORKAROUND

是创建一个mock文件,取名为matchMedia.js并添加如下代码:

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

然后,在您的测试文件中,导入您的模拟 import './matchMedia'; 只要您在每个用例中都导入它,它应该可以解决您的问题。

ALTERNATIVE OPTION

我一直 运行 解决这个问题,发现自己只是导入了太多东西,我想我会提供一个替代解决方案。

即创建setup/before.js文件,内容如下:

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

然后在 jest.config 文件中添加以下内容:

setupFiles: ['<rootDir>/通往您的 BEFORE.JS 文件的路径'],

在我决定将 react-scripts 从 3.4.1 更新到 4.0.3(因为我使用 create-react-app)之前,官方解决方法对我有用。然后我开始收到错误 Cannot read property 'matches' of undefined.

这是我找到的解决方法。安装 mq-polyfill 作为开发依赖项。

然后在 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'))
}

这对我有用。

将以下行添加到您的 setupTest.js 文件中,

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

这将为您的所有测试用例添加匹配媒体查询。

因为我使用了一个使用 window.matchMedia

的库

对我有用的是需要测试中的组件(我使用 React)和 jest.isolateModules()

中的 window.matchMedia 模拟
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 模拟(在 /__mocks__/window/match-media 内):

// Mock to solve: `TypeError: window.matchMedia is not a function`
// From 

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

如果您正在测试的组件包含 window.matchMedia() 或导入另一个组件(即 CSS 媒体查询挂钩使用 useMedia() )并且您不打算测试与之相关的任何内容,您可以通过向组件添加 window 检查来绕过调用该方法。

在下面的示例代码中,如果代码由 Jest 运行,useMedia 钩子将始终 return false。

有一个 post 关于反对模拟模块导入的论点。,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;
}

但是如果您想从方法中访问对象 return,您可以在组件本身中模拟它,而不是测试文件。查看示例用法:(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;

我开发了一个专门为此设计的库:https://www.npmjs.com/package/mock-match-media

它提出了 matchMedia 节点的完整实现。

它甚至还有一个 jest-setup 文件,您可以在您的 jest 设置中导入该文件,以将此模拟应用于您的所有测试(参见 https://www.npmjs.com/package/mock-match-media#jest):

require('mock-match-media/jest-setup);

也可以在使用前先测试window.matchMedia的类型是否为函数

示例:

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

而且测试不会再失败