'Testing MutationObserver with Jest

I wrote a script with the main purpose of adding new elements to some table's cells.

The test is done with something like that:

document.body.innerHTML = `
<body>
    <div id="${containerID}">
        <table>
            <tr id="meta-1"><td> </td></tr>
            <tr id="meta-2"><td> </td></tr>
            <tr id="meta-3"><td> </td></tr>
            <tr id="no-meta-1"><td> </td></tr>
        </table>
    </div>
</body>
`;

    const element = document.querySelector(`#${containerID}`);

    const subject = new WPMLCFInfoHelper(containerID);
    subject.addInfo();

    expect(mockWPMLCFInfoInit).toHaveBeenCalledTimes(3);

mockWPMLCFInfoInit, when called, is what tells me that the element has been added to the cell.

Part of the code is using MutationObserver to call again mockWPMLCFInfoInit when a new row is added to a table:

new MutationObserver((mutations) => {
    mutations.map((mutation) => {
        mutation.addedNodes && Array.from(mutation.addedNodes).filter((node) => {
            console.log('New row added');
            return node.tagName.toLowerCase() === 'tr';
        }).map((element) => WPMLCFInfoHelper.addInfo(element))
    });
}).observe(metasTable, {
    subtree:   true,
    childList: true
});

WPMLCFInfoHelper.addInfo is the real version of mockWPMLCFInfoInit (which is a mocked method, of course).

From the above test, if add something like that...

const table = element.querySelector(`table`);
var row = table.insertRow(0);

console.log('New row added'); never gets called. To be sure, I've also tried adding the required cells in the new row.

Of course, a manual test is telling me that the code works.

Searching around, my understanding is that MutationObserver is not supported and there is no plan to support it.

Fair enough, but in this case, how can I test this part of my code? Except manually, that is :)



Solution 1:[1]

I know I'm late to the party here, but in my jest setup file, I simply added the following mock MutationObserver class.

global.MutationObserver = class {
    constructor(callback) {}
    disconnect() {}
    observe(element, initObject) {}
};

This obviously won't allow you to test that the observer does what you want, but will allow the rest of your code's tests to run which is the path to a working solution.

Solution 2:[2]

The problem is actually appears because of JSDom doesn't support MutationObserver, so you have to provide an appropriate polyfill.

Little tricky thought may not the best solution (let's use library intend for compatibility with IE9-10).

Step 1 (install this library to devDependencies)

npm install --save-dev mutation-observer

Step 2 (Import and make global)

import MutationObserver from 'mutation-observer'
global.MutationObserver = MutationObserver 

test('your test case', () => { 
   ...
})

Solution 3:[3]

You can use mutationobserver-shim.

Add this in setup.js

import "mutationobserver-shim"

and install

npm i -D mutationobserver-shim

Solution 4:[4]

I think a fair portion of the solution is just a mindset shift. Unit tests shouldn't determine whether MutationObserver is working properly. Assume that it is, and mock the pieces of it that your code leverages.

Simply extract your callback function so it can be tested independently; then, mock MutationObserver (as in samuraiseoul's answer) to prevent errors. Pass a mocked MutationRecord list to your callback and test that the outcome is expected.

That said, using Jest mock functions to mock MutationObserver and its observe() and disconnect() methods would at least allow you to check the number of MutationObserver instances that have been created and whether the methods have been called at expected times.

const mutationObserverMock = jest.fn(function MutationObserver(callback) {
    this.observe = jest.fn();
    this.disconnect = jest.fn();
    // Optionally add a trigger() method to manually trigger a change
    this.trigger = (mockedMutationsList) => {
        callback(mockedMutationsList, this);
    };
});
global.MutationObserver = mutationObserverMock;

it('your test case', () => {
    // after new MutationObserver() is called in your code
    expect(mutationObserverMock.mock.instances).toBe(1);

    const [observerInstance] = mutationObserverMock.mock.instances;
    expect(observerInstance.observe).toHaveBeenCalledTimes(1);
});

Solution 5:[5]

Since it's not mentioned here: jsdom has supported MutationObserver for a while now.

Here's the PR implementing it https://github.com/jsdom/jsdom/pull/2398

Solution 6:[6]

Addition for TypeScript users:

declare the module with adding a file called: mutation-observer.d.ts

/// <reference path="../../node_modules/mutation-observer" />
declare module "mutation-observer";

Then in your jest file.

import MutationObserver from 'mutation-observer'
(global as any).MutationObserver = MutationObserver

Solution 7:[7]

This is a typescript rewrite of Matt's answer above.

// Test setup
const mutationObserverMock = jest
  .fn<MutationObserver, [MutationCallback]>()
  .mockImplementation(() => {
    return {
      observe: jest.fn(),
      disconnect: jest.fn(),
      takeRecords: jest.fn(),
    };
  });
global.MutationObserver = mutationObserverMock;

// Usage
new MutationObserver(() => {
  console.log("lol");
}).observe(document, {});

// Test
const observerCb = mutationObserverMock.mock.calls[0][0];
observerCb([], mutationObserverMock.mock.instances[0]);

Solution 8:[8]

Recently I had a similar problem, where I wanted to assert on something that should be set by MutationObserver and I think I found fairly simple solution.

I made my test method async and added await new Promise(process.nextTick); just before my assertion. It puts the new promise at the end on microtask queue and holds the test execution until it is resolved. This allows for the MutationObserver callback, which was put on the microtask queue before our promise, to be executed and make changes that we expect.

So in general the test should look somewhat like this:

it('my test', async () => {
    somethingThatTriggersMutationObserver();
    await new Promise(process.nextTick);
    expect(mock).toHaveBeenCalledTimes(3);
});

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 samuraiseoul
Solution 2 Purkhalo Alex
Solution 3 Codler
Solution 4 Matt
Solution 5 Epeli
Solution 6 Hannibal B. Moulvad
Solution 7 Llyle
Solution 8 cichu