Scherztest schlägt fehl: TypeError: window.matchMedia ist keine Funktion

74

Dies ist meine erste Erfahrung mit Front-End-Tests. In diesem Projekt verwende ich Jest-Snapshot-Tests und habe einen Fehler TypeError: window.matchMedia is not a functionin meiner Komponente erhalten.

Ich habe die Jest-Dokumentation durchgesehen und den Abschnitt "Manuelle Verspottungen" gefunden, aber ich habe noch keine Ahnung, wie das geht.

TIJ
quelle

Antworten:

113

Die Jest-Dokumentation enthält jetzt eine "offizielle" Problemumgehung:

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

Verspottungsmethoden, die nicht in JSDOM implementiert sind

Selrond
quelle
1
Das ist die richtige Antwort. Beachten Sie hier, dass Sie in Ihrem Test das Modell importieren MÜSSEN, BEVOR Sie die zu testende Datei importieren. zB `` `// import '../mockFile' // import '../fileToTest'` ``
ginna
Beachten Sie, dass addListenerund removeListenerveraltet sind addEventListenerund removeEventListenerstattdessen verwendet werden sollten. Das vollständige
Scheinobjekt
Wie kann der Wert geändert werden, wenn man sich so verspottet?
evolutionxbox
@evolutionxbox siehe die Antwort, die ich gerade gepostet habe, es könnte dir helfen! (Wenn Sie sich seit dem 28. Februar immer noch am Kopf kratzen!)
MaxiJonson
Wohin soll dieses Snippet gehen, um das Problem in einem Projekt global zu lösen?
bluenote10
37

Ich habe diese Technik verwendet, um eine Reihe von Spottproblemen zu lösen.

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

Oder, wenn Sie es die ganze Zeit verspotten möchten, können Sie in Ihre mocksDatei einfügen, die von Ihrem aufgerufen wird package.json: "setupFilesAfterEnv": "<rootDir>/src/tests/mocks.js",.

Referenz: setupTestFrameworkScriptFile

Maxime Beauchamp
quelle
1
Wo fügen Sie diesen Code hinzu? Wenn ich es oben in meine Testdatei einfüge, kann es matchMedia immer noch nicht finden.
Holger Sindbaek
2
@ HolgerEdwardWardlowSindbæk Ich habe meine Antwort für mehr Klarheit bearbeitet!
Maxime Beauchamp
Ich habe eine TypeError: Cannot read property 'matches' of undefinedAusnahme
Anthony Kong
Durch Hinzufügen der folgenden Eigenschaften addListener: () und removeListener: () können zusätzliche Fehler bei fehlenden Funktionen vermieden werden.
michal zagrodzki
Warum setupFilesAfterEnvund nicht setupFiles?
xtra
16

Ich habe einen matchMedia- Stub in meine Jest-Testdatei (über den Tests) eingefügt , damit die Tests bestanden werden können:

window.matchMedia = window.matchMedia || function() {
    return {
        matches: false,
        addListener: function() {},
        removeListener: function() {}
    };
};
Benjamin H. Boruff
quelle
2
und mit in der Testdatei, innerhalb von 'beschreiben' mit Scherz, schreibe ich:global.window.matchMedia = jest.fn(() => { return { matches: false, addListener: jest.fn(), removeListener: jest.fn() } })
spakmad
Wie importiere ich die Stub-Datei?
Holger Sindbaek
13

Jest verwendet jsdom , um eine Browserumgebung zu erstellen. JSDom unterstützt dies jedoch nicht, window.matchMediasodass Sie es selbst erstellen müssen.

Jests manuelle Mocks funktionieren mit Modulgrenzen, dh erfordern / importieren Anweisungen, damit sie nicht so verspottet werden können, window.matchMediawie sie sind, weil sie global sind.

Sie haben also zwei Möglichkeiten:

  1. Definieren Sie Ihr eigenes lokales matchMedia-Modul, das window.matchMedia exportiert. - Auf diese Weise können Sie ein manuelles Modell definieren, das in Ihrem Test verwendet werden soll.

  2. Definieren Sie eine Setup-Datei, die dem globalen Fenster einen Mock für matchMedia hinzufügt.

Mit einer dieser Optionen können Sie eine matchMedia-Polyfüllung als Mock verwenden, mit der zumindest Ihre Tests ausgeführt werden können, oder wenn Sie verschiedene Zustände simulieren müssen, möchten Sie möglicherweise Ihre eigenen mit privaten Methoden schreiben, mit denen Sie das Verhalten ähnlich dem konfigurieren können Scherz fsmanuell verspotten

Riscarrott
quelle
9

Ich bin gerade auf dieses Problem gestoßen und musste diese in jestGlobalMocks.ts verspotten:

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

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

Sie können die API verspotten:

describe("Test", () => {
  beforeAll(() => {
    Object.defineProperty(window, "matchMedia", {
      value: jest.fn(() => {
        return {
          matches: true,
          addListener: jest.fn(),
          removeListener: jest.fn()
        };
      })
    });
  });
});
Pål Thingbø
quelle
Ich freue mich besonders über die Einfachheit und Klarheit Ihres Ansatzes, danke für die Veröffentlichung!
Jason R Stevens CFA
3

Sie können das jest-matchmedia-mockPaket zum Testen von Medienabfragen verwenden (z. B. Änderung des Gerätebildschirms, Änderung des Farbschemas usw.).

dyakovk
quelle
1

Ich habe alle oben genannten vorherigen Antworten ohne Erfolg ausprobiert.

Das Hinzufügen von matchMedia.js zum Mocks- Ordner hat es für mich getan.

Ich habe es mit dem Inhalt von techguy2000 gefüllt :

// __mocks__/matchMedia.js
'use strict';

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

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

module.exports = window;

Und dann importiert dies in setup.js:

import matchMedia from '../__mocks__/matchMedia';

Boom! :) :)

Arakno
quelle
0

TL; DR Antwort weiter unten

In meinem Fall war die Antwort nicht genug, da window.matchMediasie immer zurückkehren würde false(oder truewenn Sie sie ändern). Ich hatte einige React-Hooks und -Komponenten, die mehrere verschiedene Abfragen mit möglicherweise unterschiedlichen abhören mussten matches.

Was ich versucht habe

Wenn Sie jeweils nur eine Abfrage testen müssen und Ihre Tests nicht auf mehreren Übereinstimmungen beruhen, jest-matchmedia-mockwar dies hilfreich. Nach dem Versuch, es 3 Stunden lang zu verwenden, habe ich jedoch verstanden useMediaQuery, dass die vorherigen Abfragen, die Sie gestellt haben , beim Anrufen nicht mehr funktionieren. Tatsächlich stimmt die Abfrage, an die Sie übergeben useMediaQuery, nur dann überein, truewenn Ihr Code window.matchMediadieselbe Abfrage aufruft , unabhängig von der tatsächlichen "Fensterbreite".

Antworten

Nachdem mir klar wurde, dass ich meine Abfragen nicht testen konnte jest-matchmedia-mock, änderte ich die ursprüngliche Antwort ein wenig, um das Verhalten dynamischer Abfragen verspotten zu können matches. Diese Lösung erfordert das css-mediaquerynpm-Paket.

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

Es wird css-mediaquerymit dem verwendet window.innerWidth, um zu bestimmen, ob die Abfrage tatsächlich mit einem fest codierten Booleschen Wert übereinstimmt. Außerdem wird die Größe von Ereignissen abgehört, die von der window.resizeToverspotteten Implementierung ausgelöst wurden , um den matchesWert zu aktualisieren .

Sie können jetzt window.resizeToin Ihren Tests die Breite des Fensters ändern, damit Ihre Aufrufe window.matchMediadiese Breite widerspiegeln. Hier ist ein Beispiel, das nur für diese Frage erstellt wurde. Ignorieren Sie also die Leistungsprobleme!

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

Hinweis: Ich habe das Verhalten beim Ändern der Fenstergröße NACH der Montage der Komponente nicht getestet

MaxiJonson
quelle