【发布时间】:2017-02-11 08:06:06
【问题描述】:
这是我的第一次前端测试体验。在这个项目中,我正在使用 Jest 快照测试并在我的组件中收到错误 TypeError: window.matchMedia is not a function。
我浏览了 Jest 文档,找到了“手动模拟”部分,但我还不知道该怎么做。
【问题讨论】:
这是我的第一次前端测试体验。在这个项目中,我正在使用 Jest 快照测试并在我的组件中收到错误 TypeError: window.matchMedia is not a function。
我浏览了 Jest 文档,找到了“手动模拟”部分,但我还不知道该怎么做。
【问题讨论】:
如果您正在测试的组件包含 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;
}
但是如果你想访问从方法返回的对象,你可以在组件本身中模拟它,而不是测试文件。查看示例用法:(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;
【讨论】:
因为我使用了一个使用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 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 {};
【讨论】:
将以下行添加到您的 setupTest.js 文件中,
global.matchMedia = global.matchMedia || function() {
return {
matches : false,
addListener : function() {},
removeListener: function() {}
}
}
这将为您的所有测试用例添加匹配媒体查询。
【讨论】:
window 对象。如果您使用 Next.js 并使用 typeof window === 'undefined' 检测服务器端执行,那么这些测试将依次中断。
官方解决方法对我有用,直到我决定将 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'))
}
这对我有用。
【讨论】:
就是创建一个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';
只要您在每个用例中都导入它,它应该可以解决您的问题。
我一直遇到这个问题,发现自己做了太多的导入,我想我会提供一个替代解决方案。
也就是创建一个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 文件的路径'],
【讨论】:
TL;DR 答案在下方
就我而言,答案还不够,因为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-mediaquery 和window.innerWidth 来确定查询ACTUALLY 是否匹配,而不是硬编码的布尔值。它还监听 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");
});
注意:在安装组件后调整窗口大小时,我没有测试此行为
【讨论】:
您可以使用jest-matchmedia-mock 包来测试任何媒体查询(如设备屏幕更改、配色方案更改等)
【讨论】:
这些家伙通过 Jest setupFiles 提供了一个非常巧妙的解决方案:
https://github.com/HospitalRun/components/pull/117/commits/210d1b74e4c8c14e1ffd527042e3378bba064ed8
【讨论】:
您可以模拟 API:
describe("Test", () => {
beforeAll(() => {
Object.defineProperty(window, "matchMedia", {
value: jest.fn(() => {
return {
matches: true,
addListener: jest.fn(),
removeListener: jest.fn()
};
})
});
});
});
【讨论】:
我尝试了上面所有的答案,但没有成功。
将 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';
轰隆隆! :)
【讨论】:
我刚刚遇到这个问题,不得不在 jestGlobalMocks.ts 中模拟这些:
Object.defineProperty(window, 'matchMedia', {
value: () => {
return {
matches: false,
addListener: () => {},
removeListener: () => {}
};
}
});
Object.defineProperty(window, 'getComputedStyle', {
value: () => {
return {
getPropertyValue: () => {}
};
}
});
【讨论】:
我一直在使用这种技术来解决一堆模拟问题。
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",.
【讨论】:
TypeError: Cannot read property 'matches' of undefined 异常
setupFilesAfterEnv 而不是setupFiles?
我将 matchMedia 存根放在我的 Jest 测试文件中(在测试之上),它允许测试通过:
window.matchMedia = window.matchMedia || function() {
return {
matches: false,
addListener: function() {},
removeListener: function() {}
};
};
【讨论】:
global.window.matchMedia = jest.fn(() => { return { matches: false, addListener: jest.fn(), removeListener: jest.fn() } })
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(),
})),
});
【讨论】:
addListener 和 removeListener 已弃用,应使用 addEventListener 和 removeEventListener 代替。完整的模拟对象可以是found in the Jest docs
Jest 使用jsdom 创建浏览器环境。然而,JSDom 不支持window.matchMedia,所以你必须自己创建它。
Jest 的 manual mocks 使用模块边界,即 require / import 语句,因此它们不适合模拟 window.matchMedia,因为它是全局的。
因此,您有两个选择:
定义您自己的本地 matchMedia 模块,用于导出 window.matchMedia。 -- 这将允许您定义一个手动模拟以在您的测试中使用。
定义一个 setup file,它将 matchMedia 的模拟添加到全局窗口。
使用这些选项中的任何一个,您都可以使用 matchMedia polyfill 作为模拟,这至少可以让您的测试运行,或者如果您需要模拟不同的状态,您可能希望使用私有方法编写自己的状态,允许您对其进行配置行为类似于开玩笑fs manual mock
【讨论】: