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

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

我浏览了 Jest 文档,找到了“手动模拟”部分,但是我还不知道如何做到这一点。

97331 次浏览

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

Jest 的 手动模拟使用模块边界,也就是请求/导入语句,因此它们不适合模拟 window.matchMedia,因为它是全局的。

因此,你有两个选择:

  1. 定义您自己的本地 match Media 模块,该模块导出 window.match Media。——这将允许您定义在测试中使用的手动模拟。

  2. 定义一个 安装文件,它向全局窗口添加一个 match Media 模拟。

有了这两个选项,你可以使用一个 火柴媒体填充材料作为模拟,这样至少可以让你的测试运行,或者如果你需要模拟不同的状态,你可能想编写自己的私有方法,允许你配置它的行为类似于 Jest fs手动模拟

我在 Jest 测试文件中(在测试之上)放了一个 火柴媒体存根,它允许测试通过:

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

我刚刚遇到了这个问题,不得不在 GlobalMocks.ts 上嘲笑它们:

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


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

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

将 match Media.js 添加到 嘲笑文件夹,为我做到了这一点。

我把它装满了 Techguy2000的内容:

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

未在 JDOM 中实现的模拟方法

你可以模仿 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

Enter image description here

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

下面进一步回答

在我的情况下,答案是不够的,因为 window.matchMedia总是返回 false(或 true,如果你改变它)。我有一些 React 钩子和组件,它们需要用可能不同的 matches来侦听 多重不同查询。

我尽力了

如果一次只需要测试一个查询,并且测试不依赖于多个匹配,那么 jest-matchmedia-mock非常有用。然而,在尝试使用它3个小时之后,我了解到当您调用 useMediaQuery时,以前的查询不再起作用。实际上,不管实际的“窗口宽度”如何,只要代码使用相同的查询调用 window.matchMedia,传递给 useMediaQuery的查询就会与 true匹配。

回答我

在意识到实际上无法用 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来确定查询 事实上是否匹配,而不是使用硬编码的布尔值。它还侦听由 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");
});

注意: 在安装组件后调整窗口大小时,我还没有测试这种行为

开玩笑

是创建一个名为 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'; 只要你在每个用例中导入它,它就能解决你的问题。

备选方案

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

它将创建一个 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>/回到你之前的 JS 档案'],

在我决定将 react-scripts从3.4.1升级到4.0.3(因为我使用 create-response-app)之前,官方的解决方案一直对我有用。然后我开始得到一个错误 Cannot read property 'matches' of undefined

所以我找到了一个解决方案,将 Mq- 填充材料作为开发依赖项安装。

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

模拟(在 /__mocks__/window/match-media内) :

// Mock to solve: `TypeError: window.matchMedia is not a function`
// From https://stackoverflow.com/a/53449595/5923666


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 () ,而且你并不打算测试任何与之相关的东西,你可以通过在组件中添加一个窗口检查来绕过调用这个方法。

在下面的示例代码中,如果 Jest 运行的代码,useMedia 挂钩将始终返回 false。

有一篇关于反对模拟模块导入的参数的文章,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;
}


但是如果您想访问从方法返回的对象,您可以在组件本身中模拟它,而不是测试文件。见使用范例: (源代码链接)


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 设置中导入它,以便将这个 mock 应用到所有的测试中(参见 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
}

测试再也不会失败了

如果您使用的是类型脚本,请将下面的行放在 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(),
};
};