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

185

这是我的第一次前端测试经验。在这个项目中,我使用Jest快照测试,并在我的组件内遇到了一个错误TypeError:window.matchMedia不是函数

我查阅了Jest的文档,发现了“手动模拟”部分,但是我还没有任何关于如何做到这一点的想法。

20个回答

279

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

模拟在JSDOM中未实现的方法


5
这是正确的答案。请注意,在测试中,您必须先导入模拟文件,然后才能导入要测试的文件。例如:// 导入'../mockFile' // 导入'../fileToTest' - ginna
1
请注意,addListenerremoveListener 已被弃用,应改为使用 addEventListenerremoveEventListener。完整的模拟对象可以在 Jest 文档中找到:https://jestjs.io/docs/en/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom - nerdyman
1
@evolutionxbox看一下我刚刚发布的答案,可能会对你有所帮助!(如果自2月28日以来你仍然在苦思冥想!) - MaxiJonson
12
这个片段应该放在哪里,以便在整个项目中解决问题? - bluenote10
2
测试一直失败,直到我使用了这里提供的解决方案 - https://dev59.com/91IG5IYBdhLWcg3whAIW - Gyro
显示剩余9条评论

71

我一直在使用这种技术来解决许多 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",

参考:setupTestFrameworkScriptFile


2
你把这段代码添加在哪里?如果我把它添加到我的测试文件顶部,那么它仍然找不到matchMedia。 - Holger Sindbaek
2
@HolgerEdwardWardlowSindbæk 我修改了我的回答以提供更清晰的解释! - Maxime Beauchamp
13
我收到了一个“TypeError:Cannot read property 'matches' of undefined”异常。 - Anthony Kong
添加addListener:()和removeListener:()属性有助于避免由于缺少函数而导致的额外失败。 - michal zagrodzki
为什么是 setupFilesAfterEnv 而不是 setupFiles - xtra

49

我在Jest测试文件中添加了一个 matchMedia 存根(放在测试上面),这样测试就能通过:

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

2
在测试文件中,在describe块内使用jest,我写了以下代码: global.window.matchMedia = jest.fn(() => { return { matches: false, addListener: jest.fn(), removeListener: jest.fn() } }) - spakmad
你怎么导入存根文件? - Holger Sindbaek
2
这适用于一个单元测试,如果您有多个组件存在相同的问题,则需要将此片段分别放入每个测试中。通常我们希望避免重写相同的代码,但如果这对您有用,这是一个很好的快速解决方案。 - humans
不是应该用 addEventListenerremoveEventListener 吗? - jayarjo

27

JESTS 官方解决方法 Mock 未在 Jsdom 中实现的方法

创建一个名为 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文件的路径'],


这就是方法。 - LessQuesar

17
Jest使用jsdom创建浏览器环境。然而,JSDom不支持window.matchMedia,因此您需要自己创建它。
Jest的手动模拟与模块边界一起工作,即需要/导入语句,因此它们不适用于直接模拟全局变量window.matchMedia
因此,您有两个选项:
  1. 定义自己的本地matchMedia模块,该模块导出window.matchMedia。-- 这将允许您定义手动模拟以在测试中使用。

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

使用这两个选项之一,您可以使用matchMedia polyfill作为模拟,这至少可以让您的测试运行,如果您需要模拟不同的状态,则可能需要编写自己的私有方法,以允许您配置其行为类似于Jestfs手动模拟

15

在你的setupTest.js文件中添加以下行:

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

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


2
这对我非常有用,因为所有其他的修复方法都扩展了 window 对象。如果你正在使用 Next.js 并检测服务器端执行,可以使用 typeof window === 'undefined',那么这些测试将会失败。 - Tait Brown
你救了我的一天。 - MD Ashik
这是React 18中修复"TypeError: Cannot read properties of undefined (reading 'matches') at updateMatch"的解决方法。 - naXa stands with Ukraine

13

您可以模拟API:

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

1
我特别喜欢你的方法的简单和清晰,感谢你的发布! - Jason R Stevens CFA
1
即使没有这行代码,它对我来说也工作正常:matches: true, - Alex Gongadze

11
你可以使用 jest-matchmedia-mock 包来测试任何媒体查询(例如设备屏幕变化、色彩方案变化等)。

2
到目前为止最有帮助的答案...非常好用,谢谢! :) - Bozhidar Petrov

11

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

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

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

我应该把它添加到哪里?我尝试将其添加到setupFile中,但它不起作用。 - conor909
对我来说,最终是由“setupFile”引用的文件。 - techguy2000

5

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

注意:我尚未测试在组件挂载后调整窗口大小时的行为。

1
在所有的解决方案中,这是唯一一个真正保留了 window.matchMedia 功能的方法,如果您的应用程序的功能/布局等依赖于媒体查询(像大多数响应式应用程序一样),那么这一点至关重要。通过以这种方式模拟 matchMedia 函数,您可以动态设置窗口大小并在测试套件中测试相应的行为。非常感谢 @MaxiJonson! - Mo Morsi
在挂载后调整窗口大小似乎没有任何作用。要在渲染组件中看到更新后的窗口值,我必须手动重新渲染它(使用@testing-library/react)。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接