开玩笑的测试失败:TypeError:window.matchMedia不是一个函数


83

这是我的第一个前端测试经验。在这个项目中,我正在使用Jest快照测试,并且TypeError: window.matchMedia is not a function组件内部出现错误。

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

Answers:


131

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中未实现的模拟方法


1
这是正确的答案。请注意,在测试中,必须在导入要测试的文件之前导入模拟。例如```//进口'../mockFile' //进口'../fileToTest'```
金纳

请注意,addListenerremoveListener已弃用,addEventListenerremoveEventListener应改为使用。完整的模拟对象可以在Jest文档中找到
nerdyman '19

这样嘲笑时如何更改值?
evolutionxbox

@evolutionxbox看到我刚刚发布的答案,可能会对您有所帮助!(如果自2月28日以来您仍在挠头!)
MaxiJonson,

该代码段应该在哪里解决全局项目中的问题?
bluenote10

38

我一直在使用这种技术来解决许多模拟问题。

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


1
您在哪里添加此代码?如果将其添加到测试文件的顶部,则仍然找不到matchMedia。
Holger Sindbaek '17

2
@HolgerEdwardWardlowSindbæk为了更加清晰,我编辑了答案!
Maxime Beauchamp's

1
我有一个TypeError: Cannot read property 'matches' of undefined例外
安东尼·孔

添加以下属性addListener:()和removeListener:()有助于避免因缺少函数而导致的其他失败。
michal zagrodzki,

为什么setupFilesAfterEnvsetupFiles呢?
xtra

18

我在我的Jest测试文件中放置了matchMedia存根(在测试之上),该文件允许测试通过:

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

2
并在测试文件中使用玩笑“描述”内部,我写道:global.window.matchMedia = jest.fn(() => { return { matches: false, addListener: jest.fn(), removeListener: jest.fn() } })
spakmad

如何导入存根文件?
Holger Sindbaek '17

14

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

Jest的手动模拟具有模块边界,即require / import语句,因此它们不适合按window.matchMedia原样进行模拟,因为它是全局的。

因此,您有两种选择:

  1. 定义自己的本地matchMedia模块,该模块导出window.matchMedia。-这样您就可以定义一个要在测试中使用的手动模拟。

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

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


9

我刚遇到此问题,不得不在jestGlobalMocks.ts中模拟它们:

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

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

7

您可以模拟API:

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

我特别喜欢您的方法简单明了,感谢您的发帖!
杰森·史蒂文斯

3

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


1

我尝试了上述所有以上答案,但均未成功。

添加matchMedia.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';

繁荣!:)



0

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

它采用css-mediaquerywindow.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");
});

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.