Jest测试失败:TypeError:window.matchMedia不是函数

TIJ*_*TIJ 34 reactjs jestjs

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

我通过jest文档,我找到了"手动模拟"部分,但我还不知道如何做到这一点.

Sel*_*ond 34

jest docs现在有一个"官方"解决方法:

window.matchMedia = 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(),
  };
});
Run Code Online (Sandbox Code Playgroud)

模拟JSDOM中未实现的方法

  • 该片段应该放在哪里来解决项目中的全局问题? (3认同)
  • 这是正确的答案。请注意,在您的测试中,您必须在导入正在测试的文件之前导入模拟。例如``` // 导入'../mockFile' // 导入'../fileToTest' ``` (2认同)
  • 测试对我来说一直失败,直到我使用这里提供的解决方案 - /sf/ask/4536941321/ (2认同)

Gab*_*MSC 28

JESTS官方解决方法

就是创建一个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(),
    })),
});
Run Code Online (Sandbox Code Playgroud)

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

另类选择

我一直遇到这个问题,发现自己做了太多进口,以为我会提供替代解决方案。

这是创建一个 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(),
    })),
});
Run Code Online (Sandbox Code Playgroud)

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

setupFiles: ['<rootDir>/路由到您的 before.js 文件'],


Max*_*amp 23

我一直在使用这种技术来解决一堆嘲弄问题.

describe("Test", () => {
  beforeAll(() => {  
    Object.defineProperty(window, "matchMedia", {
      value: jest.fn(() => { return { matches: true } })
    });
  });
});
Run Code Online (Sandbox Code Playgroud)

或者如果你想一直嘲笑它,你可以放入你的mocks文件中调用package.json: "setupTestFrameworkScriptFile": "<rootDir>/src/tests/mocks.js",

ref:setupTestFrameworkScriptFile

  • 我收到了一个 `TypeError: Cannot read property 'matches' of undefined` 异常 (4认同)
  • 在哪里添加这段代码?如果我将它添加到测试文件的顶部,那么它仍然找不到 matchMedia。 (3认同)
  • @HolgerEdwardWardlowSindbæk为了更加清晰,我编辑了答案! (2认同)

小智 16

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

global.matchMedia = global.matchMedia || function() {
    return {
        matches : false,
        addListener : function() {},
        removeListener: function() {}
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 这对我来说非常有用,因为所有其他修复都扩展了“window”对象。如果您使用 Next.js 并使用“typeof window === 'undefined'”检测服务器端执行,那么这些测试将依次中断。 (2认同)

Ben*_*uff 13

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

window.matchMedia = window.matchMedia || function() {
    return {
        matches : false,
        addListener : function() {},
        removeListener: function() {}
    };
};
Run Code Online (Sandbox Code Playgroud)

  • 并且在测试文件中,在'describe'中使用jest,我写道:`global.window.matchMedia = jest.fn(()=> {return {matches:false,addListener:jest.fn(),removeListener: jest.fn()}})` (2认同)
  • 这适用于一个单元测试,如果您有多个组件存在相同的问题,则需要将此代码片段单独放入每个测试中。通常我们希望避免重写相同的代码,但如果这对您有用,那么这是一个很好的快速解决方案。 (2认同)

ris*_*ott 12

Jest使用jsdom创建浏览器环境.但JSDom不支持,window.matchMedia因此您必须自己创建它.

Jest的手动模拟使用模块边界,即需要/导入语句,因此它们不适合模拟window.matchMedia,因为它是全局的.

因此,您有两种选择:

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

  2. 定义一个设置文件,将matchMedia的模拟添加到全局窗口.

使用这些选项中的任何一个,您可以使用matchMedia polyfill作为模拟,这至少允许您的测试运行,或者如果您需要模拟不同的状态,您可能希望使用私有方法编写自己的状态,允许您配置它的行为类似于Jest fs手动模拟


tec*_*000 8

刚刚遇到这个问题.我不得不在jestGlobalMocks.ts中嘲笑这些:

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

    Object.defineProperty(window, 'getComputedStyle', {
      value: () => {
        return {
          getPropertyValue: () => {}
        };
      }
    });
Run Code Online (Sandbox Code Playgroud)


小智 8

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

  • 迄今为止最有帮助的答案...就像魅​​力一样,谢谢!:) (2认同)

Pål*_*gbø 7

您可以模拟 API:

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


Bri*_*ena 6

如果您使用的是打字稿,请将以下行放入 setupTests.ts 文件中。这对我有用:

 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(),
    };
  };
Run Code Online (Sandbox Code Playgroud)


Max*_*son 5

TL;DR 答案在下面

就我而言,答案是不够的,因为window.matchMedia总是会返回false(或者true如果您更改它)。我有一些 React 挂钩和组件,需要用可能不同matches.

我尝试过的

如果您一次只需要测试一个查询并且您的测试不依赖于多个匹配,那么这jest-matchmedia-mock是有用的。然而,根据我在尝试使用它 3 小时后了解到的情况,当您致电 时useMediaQuery,您之前所做的查询将不再起作用。事实上,只要您的代码使用同一查询调用,您传入的查询useMediaQuery就会匹配,而不管实际的“窗口宽度”如何。truewindow.matchMedia

回答

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

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"));
    },
});
Run Code Online (Sandbox Code Playgroud)

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

您现在可以window.resizeTo在测试中使用来更改窗口的宽度,以便您的调用能够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");
});
Run Code Online (Sandbox Code Playgroud)

注意:在安装组件后调整窗口大小时,我尚未测试此行为


Dmi*_*bov 5

官方的解决方法对我有用,直到我决定从 3.4.1 更新react-scripts到 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'))
}
Run Code Online (Sandbox Code Playgroud)

这对我有用。