【问题标题】:Jest test fails : TypeError: window.matchMedia is not a function开玩笑测试失败:TypeError:window.matchMedia 不是函数
【发布时间】:2017-02-11 08:06:06
【问题描述】:

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

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

【问题讨论】:

    标签: reactjs jestjs


    【解决方案1】:

    如果您正在测试的组件包含 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;
    

    【讨论】:

      【解决方案2】:

      因为我使用了一个使用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 {};
      

      【讨论】:

        【解决方案3】:

        将以下行添加到您的 setupTest.js 文件中,

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

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

        【讨论】:

        • 这对我来说非常有用,因为所有其他修复都扩展了 window 对象。如果您使用 Next.js 并使用 typeof window === 'undefined' 检测服务器端执行,那么这些测试将依次中断。
        • 你拯救了我的一天
        【解决方案4】:

        官方解决方法对我有用,直到我决定将 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'))
        }
        

        这对我有用。

        【讨论】:

          【解决方案5】:

          开玩笑OFFICIAL WORKAROUND

          就是创建一个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'; 只要您在每个用例中都导入它,它应该可以解决您的问题。

          ALTERNATIVE OPTION

          我一直遇到这个问题,发现自己做了太多的导入,我想我会提供一个替代解决方案。

          也就是创建一个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: ['&lt;rootDir&gt;/到您的 BEFORE.JS 文件的路径'],

          【讨论】:

            【解决方案6】:

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

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

            【讨论】:

            • 在所有解决方案中,这是唯一真正保留 window.matchMedia 功能的解决方案,如果您的应用程序的功能/布局等依赖于媒体查询(大多数反应式应用程序也是如此),这一点至关重要这些天确实如此)。通过以这种方式模拟 matchMedia 函数,您可以动态设置窗口大小并在测试套件中测试相应的行为。非常感谢@MaxiJonson!
            【解决方案7】:

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

            【讨论】:

            • 迄今为止最有帮助的答案......就像一个魅力,谢谢! :)
            【解决方案8】:

            这些家伙通过 Jest setupFiles 提供了一个非常巧妙的解决方案:

            https://github.com/HospitalRun/components/pull/117/commits/210d1b74e4c8c14e1ffd527042e3378bba064ed8

            【讨论】:

              【解决方案9】:

              您可以模拟 API:

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

              【讨论】:

              • 我特别喜欢您的简单明了的方法,感谢您的发帖!
              【解决方案10】:

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

              将 matchMedia.js 添加到 mocks 文件夹中,为我做了。

              我填了techguy2000's content:

              // __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';
              

              轰隆隆! :)

              【讨论】:

                【解决方案11】:

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

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

                【讨论】:

                • 我在哪里添加这个?我尝试添加到 setupFile 但它不起作用
                • 对我来说,归根结底是“setupFile”引用的文件
                【解决方案12】:

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

                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": "&lt;rootDir&gt;/src/tests/mocks.js",.

                参考:setupTestFrameworkScriptFile

                【讨论】:

                • 您在哪里添加此代码?如果我将它添加到我的测试文件的顶部,那么它仍然找不到 matchMedia。
                • @HolgerEdwardWardlowSindbæk 为了更清楚,我编辑了我的答案!
                • 我遇到了TypeError: Cannot read property 'matches' of undefined 异常
                • 添加以下属性 addListener: () 和 removeListener: () 有助于避免因缺少功能而导致的额外失败。
                • 为什么是setupFilesAfterEnv 而不是setupFiles
                【解决方案13】:

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

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

                【讨论】:

                • 并在测试文件中,在“describe”中使用玩笑,我写道:global.window.matchMedia = jest.fn(() =&gt; { return { matches: false, addListener: jest.fn(), removeListener: jest.fn() } })
                • 如何导入存根文件?
                • 这适用于一个单元测试,如果您有多个组件有相同的问题,您需要将此 sn-p 单独放入每个测试中。通常我们希望避免重写相同的代码,但如果这对您有用,这是一个很好的快速解决方案。
                【解决方案14】:

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

                Mocking methods which are not implemented in JSDOM

                【讨论】:

                • 这是正确的答案。请注意,在您的测试中,您必须在导入您正在测试的文件之前导入模拟。例如 ``` // 导入 '../mockFile' // 导入 '../fileToTest' ```
                • 请注意 addListenerremoveListener 已弃用,应使用 addEventListenerremoveEventListener 代替。完整的模拟对象可以是found in the Jest docs
                • 这样模拟时如何更改值?
                • @evolutionxbox 看到我刚刚发布的答案,它可能会帮助你! (如果您自 2 月 28 日起还在摸不着头脑!)
                • sn-p 应该去哪里解决项目中的全局问题?
                【解决方案15】:

                Jest 使用jsdom 创建浏览器环境。然而,JSDom 不支持window.matchMedia,所以你必须自己创建它。

                Jest 的 manual mocks 使用模块边界,即 require / import 语句,因此它们不适合模拟 window.matchMedia,因为它是全局的。

                因此,您有两个选择:

                1. 定义您自己的本地 matchMedia 模块,用于导出 window.matchMedia。 -- 这将允许您定义一个手动模拟以在您的测试中使用。

                2. 定义一个 setup file,它将 matchMedia 的模拟添加到全局窗口。

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

                【讨论】:

                  猜你喜欢
                  • 2020-08-24
                  • 2018-02-13
                  • 2017-11-19
                  • 2020-03-11
                  • 2020-10-08
                  • 2017-08-11
                  • 2019-05-17
                  • 2018-07-20
                  • 2020-08-19
                  相关资源
                  最近更新 更多