Testing MutationObserver with Jest
Asked Answered
P

10

26

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 :)

Pahlavi answered 15/2, 2018 at 14:37 Comment(2)
You'll need to create a mock implementation of MutationObserver that behaves how you need it to.Knacker
@AlexRobertson Mocking would not a problem for an imported module, but I have no idea of how to mock something defined as MutationObserver = window.MutationObserver || window.WebKitMutationObserver; which is how I've learned to deal with them.Pahlavi
B
36

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.

Banyan answered 31/5, 2018 at 14:3 Comment(3)
Just remember, this WILL NOT test the mutation observer's stuff. I still have not found a good solution to that so the mutation observer's part in my code is still scary.Banyan
You can use Jest mock functions to mock MutationObserver. This allows you to test instances of it in your code. See my answer for an example.Ephialtes
For some reason that won't work for me. I have to define it in the test file, which is really a pain.Rainwater
E
14

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

Ephialtes answered 1/4, 2020 at 15:4 Comment(2)
This looks like exactly what I want! Except in my setup its doesn't have any effect. I'm getting this error "[BootstrapVue warn]: observeDom: Requires MutationObserver support." Doing vue unit testing using jest, bootstrapVue, vue-test-utils. Any further hints would be awesome.Dunite
@rodhoward While I can't speak to what might be happening with BootstrapVue, my example was used in a vue-cli project using vue-test-utils. We were using MutationObserver manually in our own code, though. BootstrapVue has a hasMutationObserverSupport check in src/utils/env.js. My best guess is that it has something to do with it being in node_modules, and how jest handles that?Ephialtes
I
10

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', () => { 
   ...
})
Inevasible answered 14/9, 2019 at 8:17 Comment(1)
There is zero need for a 3rd-party package. You can simply mock it yourself.Mucoviscidosis
S
10

You can use mutationobserver-shim.

Add this in setup.js

import "mutationobserver-shim"

and install

npm i -D mutationobserver-shim
Spratt answered 6/12, 2019 at 9:22 Comment(1)
There is zero need for a 3rd-party package. You can simply mock it yourself.Mucoviscidosis
E
4

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

Eastbound answered 18/12, 2020 at 8:30 Comment(1)
for testing I add a 1ms timeout to wait for the mutation observer to kick in: ``` const timeout = (ms) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; await timeout(1); ```Obumbrate
H
3

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]);

Higley answered 25/6, 2021 at 3:23 Comment(0)
A
1

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);
});
Aide answered 22/4, 2022 at 5:17 Comment(0)
R
0

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
Rambutan answered 21/11, 2019 at 10:0 Comment(0)
V
0

very late to the party but here is how I mocked MutationObserver using sinon.

    sinon.stub(global, 'MutationObserver').callsFake(() =>
    {
        return new class
        {
            constructor(callback) { }
            disconnect() { }
            observe(element, initObject) { }
        }
    });
Virgenvirgie answered 13/6, 2023 at 21:14 Comment(0)
C
0

You can test you complete functionality by implementing Mock class on window object as mention below:

 class MutationObserver {
    callback = null;
    constructor( callback ) {
        this.callback = callback;
    }
    observe( observe, options = {} ) {
        this.callback();
        return this;
    }
    disconnect() { return this; }
}

After this you need allocate this class on window object

window.MutationObserver = MutationObserver;

Then your below functionality will work smoothly

containerObserver = new MutationObserver(() => {});
containerObserver.observe( anyDOMElementToBeObserved , { childList: true, subtree: true } );
Cylindrical answered 27/5 at 7:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.