这是我的第一次前端测试经验。在这个项目中,我使用Jest快照测试,并在我的组件内遇到了一个错误TypeError:window.matchMedia不是函数
。
我查阅了Jest的文档,发现了“手动模拟”部分,但是我还没有任何关于如何做到这一点的想法。
现在 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(),
})),
});
我一直在使用这种技术来解决许多 mock 相关的问题。
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(),
}))
});
});
});
或者,如果你想一直模拟它,可以将其放入由package.json
调用的mocks
文件中:
"setupFilesAfterEnv": "<rootDir>/src/tests/mocks.js",
setupFilesAfterEnv
而不是 setupFiles
? - xtra我在Jest测试文件中添加了一个 matchMedia 存根(放在测试上面),这样测试就能通过:
window.matchMedia = window.matchMedia || function() {
return {
matches: false,
addListener: function() {},
removeListener: function() {}
};
};
describe
块内使用jest,我写了以下代码:
global.window.matchMedia = jest.fn(() => { return { matches: false, addListener: jest.fn(), removeListener: jest.fn() } })
- spakmadaddEventListener
和 removeEventListener
吗? - jayarjo创建一个名为 matchMedia.js
的 mock 文件,并添加以下代码:
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文件的路径'],
window.matchMedia
,因此您需要自己创建它。window.matchMedia
。定义自己的本地matchMedia模块,该模块导出window.matchMedia。-- 这将允许您定义手动模拟以在测试中使用。
定义一个设置文件,将matchMedia的模拟添加到全局窗口。
fs
手动模拟。在你的setupTest.js
文件中添加以下行:
global.matchMedia = global.matchMedia || function() {
return {
matches : false,
addListener : function() {},
removeListener: function() {}
}
}
这将为您的所有测试用例添加匹配媒体查询。
window
对象。如果你正在使用 Next.js 并检测服务器端执行,可以使用 typeof window === 'undefined'
,那么这些测试将会失败。 - Tait Brown您可以模拟API:
describe("Test", () => {
beforeAll(() => {
Object.defineProperty(window, "matchMedia", {
value: jest.fn(() => {
return {
matches: true,
addListener: jest.fn(),
removeListener: jest.fn()
};
})
});
});
});
matches: true,
。 - Alex Gongadzejest-matchmedia-mock
包来测试任何媒体查询(例如设备屏幕变化、色彩方案变化等)。我刚遇到了这个问题,不得不在jestGlobalMocks.ts中模拟这些内容:
Object.defineProperty(window, 'matchMedia', {
value: () => {
return {
matches: false,
addListener: () => {},
removeListener: () => {}
};
}
});
Object.defineProperty(window, 'getComputedStyle', {
value: () => {
return {
getPropertyValue: () => {}
};
}
});
TL;DR:在处理多个可能具有不同匹配项的查询时,使用jest-matchmedia-mock
测试只能测试一个查询并且无法重用之前的查询结果。如果需要模拟动态查询匹配项的行为,则需要使用css-mediaquery
npm包。
在我的情况下,这个答案还不够好,因为window.matchMedia
总是会返回false
(或者如果更改就返回true
)。我有一些React hooks和组件需要监听可能具有不同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
来确定查询是否与实际匹配,而不是硬编码的布尔值。它还监听由 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");
});
@testing-library/react
)。 - undefined
// 导入'../mockFile' // 导入'../fileToTest'
- ginnaaddListener
和removeListener
已被弃用,应改为使用addEventListener
和removeEventListener
。完整的模拟对象可以在 Jest 文档中找到:https://jestjs.io/docs/en/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom - nerdyman