How to mock ResizeObserver to work in unit tests using react testing library
Asked Answered
C

9

78

If anyone can help, I have a custom hook that uses ResizeObserver to change the width of a component. My problem is that when I go to run my units test it breaks all my tests and looking at the snapshot it is not rendering all the elements in the dom. It was working before until I implemented the ResizeObserver. Does anyone know if there is a way I jest.mock the ResizeObserver to not undefined. Or other suggestions.

import * as React from 'react';
import ResizeObserver from 'resize-observer-polyfill';

const useResizeObserver = (ref: { current: any }) => {
    const [dimensions, setDimensions] = React.useState<DOMRectReadOnly>();
    React.useEffect(() => {
        const observeTarget = ref.current;
        const resizeObserver = new ResizeObserver((entries) => {
            entries.forEach((entry) => {
                setDimensions(entry.contentRect);
            });
        });
        resizeObserver.observe(observeTarget);
        return () => {
            resizeObserver.unobserve(observeTarget);
        };
    }, [ref]);
    return dimensions;
};

export default useResizeObserver;



import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import mockFetchProfileActivity from '../../../services/mocks/fetch-profile-activity';
import BarChart from './BarChart';

const component = <BarChart userActivity={mockFetchProfileActivity} />;

describe('Render barElement Chart component', () => {
    const observers: any[] = [];
    let resizeHandler: (observers: any[]) => void;
    (window as any).ResizeObserver = (e: any) => {
        resizeHandler = e;

        return {
            observe(element: any) {
                observers.push(element);
            },
            unobserve(element: any) {
                const i = observers.indexOf(element);
                if (i !== -1) {
                    observers.splice(i, 1);
                }
            }
        };
    };

    it('Matches the snapshot', () => {
        // resizeHandler(observers);
        const container = render(component);
        expect(container).toMatchSnapshot();
    });

    it('when clicking on a chart barElement drilldown "challenges" are shown', async () => {
        // arrange
        const componentRender = render(component);
        waitFor(() => resizeHandler(observers));

        // act
        const barElement = componentRender.container.querySelector('svg rect');

        if (barElement) userEvent.click(barElement);

        // assert
        expect(screen.getByText('Challenge 1')).toBeInTheDocument();
    });
});
Chicanery answered 27/10, 2020 at 15:48 Comment(0)
P
9

I had similar issue using Create React App setup.

If that is your case, you can create a file in your root directory called setupTest.js and add the following code:

  import '@testing-library/jest-dom/extend-expect';
  import 'jest-extended';
    
  jest.mock('./hooks/useResizeObserver', () => () => ({
    __esModule: true,
    default: jest.fn().mockImplementation(() => ({
        observe: jest.fn(),
        unobserve: jest.fn(),
        disconnect: jest.fn(),
    })),
  }));

You can find more information to configure the test environment for Create React App here and the ResizeObserver API here

Peder answered 1/1, 2021 at 23:47 Comment(5)
That looks fine, the only problem is that I get an error, have you actually got this working this is the error I get. Property 'ResizeObserver' does not exist on type 'Global & typeof globalThis'Chicanery
Hi, yes for me is working fine. If I remove the code I get this error This browser does not support ResizeObserver out of the box. See: https://github.com/react-spring/react-use-measure/#resize-observer-polyfills. Did you remove the polyfill? As with the code above you wouldn't need it. If that is the case maybe is a version issue. I am using react-scripts 4.0.0Peder
Sorry just forgot to mention that the error that I'm getting (if I remove the resize observer mock) comes from a library that I am using in my project (@visx/tooltip). In your case the error might be different because you are using different librariesPeder
I am not using any libraries just a resizeObserver API in a custom hook, as not necessary to use a library. I am using react-scripts 4.0.1 but the error is more a typescript error, not a browser error.Chicanery
Ups sorry, the solution proposed above only works in Javascript. I just found an answer that could help here What about if instead global.ResizeObserver = resizeObserverMock; you add const globalAny:any = global; globalAny.ResizeObserver = resizeObserverMock;. Let me know if that works and I'll edit my answerPeder
T
80

I've added to setupTests.js/ts next code:

global.ResizeObserver = jest.fn().mockImplementation(() => ({
    observe: jest.fn(),
    unobserve: jest.fn(),
    disconnect: jest.fn(),
}))

Edit: Add these lines below the imports above the describe method

Took answered 14/1, 2022 at 0:3 Comment(4)
That worked for me in stencil js with jest testing. ThanksCoelho
Thanks! this worked for render a component that was using recharts, i was stuck for the entire day, im able to move on thanks to your answer <3Sanmiguel
replacing global with window worked for me.Colettacolette
This worked with Vitest by changing jest.fn to vi.fn.Beseem
M
65

I chose to add the polyfill as a dev dependency and add the following line to setupTests.js/ts:

global.ResizeObserver = require('resize-observer-polyfill')
Mayonnaise answered 8/4, 2021 at 15:5 Comment(10)
Very slick, and worked perfectly. I like it! To get your types to match up nicely, swap it out for the import equivalent, and assign it in the next line.Albanese
Nice! I had to do window.ResizeObserver = require... but then it worked without an issue.Effeminize
Simplest solution by far, this worked for my monorepo powered by nx.dev. I did have to add into my jest.preset.js configuration (setupFilesAfterDev option with the value being the location to setupTests.js) as nx doesn't create setupTests.js file by default.Cytolysis
you made my day and my week!!!Illuminate
i had to add setupFiles: ['./setupTests.ts'], to jest.config.js but then this worked tooCarboy
...that said, having successfully got the polyfill to rid me of errors, i subsequently discovered that ResizeObserver was returning widths and heights of 0 or undefined ... and so had to mock it anyway! (in a manner similar to @silveste answer below)Carboy
One liner helped after lot of efforts. That's was simplest solution. ThanksBoelter
This totally made my day, I only had to change it from global.require... to window.require... just like Alex Wally mentioned and it started working, thanks!Imprescriptible
Thanks, quick and simple.Schwa
Simple and great answer, using NextJs 13 and SurveyJs I had to use: useEffect(() => { if (typeof window !== 'undefined') { window.ResizeObserver = require('resize-observer-polyfill').default } }, [])Cotangent
O
41

Mock the ResizeObserver:

class ResizeObserver {
    observe() {
        // do nothing
    }
    unobserve() {
        // do nothing
    }
    disconnect() {
        // do nothing
    }
}

window.ResizeObserver = ResizeObserver;
export default ResizeObserver;

sample.test.js

import ResizeObserver from './__mocks__/ResizeObserver';
import module from 'sample';

describe('module', ()=> {
     it('returns an instance of ResizeObserver', () => {
           // do something that uses the resize observer
           // NOTE: The actual observe handler would not be called in jsdom anyway as no resize would be triggered.
           // e.g.
           expect(module.somethingThatReturnAReference to the resize observer).toBeInstanceOf(ResizeObserver);
        });
});

source

Octameter answered 1/12, 2020 at 17:28 Comment(3)
And how I ca test what's going on in the resizeObserver callback? For example, I have some changes to an HTML element style, that I need to test. How?Pelotas
Yeah, I'm still clueless of how to now test any of the logic inside the ResizeObserver :(Amah
I had to do this as my test says that it does not have jestBinominal
R
17

Building upon the already excellent answers, here is what I did to get my React Testing library tests running

Take a dependency on the required polyfill in package.json

"devDependencies": {
  ...
  "resize-observer-polyfill": "^1.5.1",
  ...
}

Update the setupTests.ts file as follow.

import * as ResizeObserverModule from 'resize-observer-polyfill';

(global as any).ResizeObserver = ResizeObserverModule.default;

Now your tests should run fine.

Radiancy answered 8/6, 2022 at 9:53 Comment(0)
P
9

I had similar issue using Create React App setup.

If that is your case, you can create a file in your root directory called setupTest.js and add the following code:

  import '@testing-library/jest-dom/extend-expect';
  import 'jest-extended';
    
  jest.mock('./hooks/useResizeObserver', () => () => ({
    __esModule: true,
    default: jest.fn().mockImplementation(() => ({
        observe: jest.fn(),
        unobserve: jest.fn(),
        disconnect: jest.fn(),
    })),
  }));

You can find more information to configure the test environment for Create React App here and the ResizeObserver API here

Peder answered 1/1, 2021 at 23:47 Comment(5)
That looks fine, the only problem is that I get an error, have you actually got this working this is the error I get. Property 'ResizeObserver' does not exist on type 'Global & typeof globalThis'Chicanery
Hi, yes for me is working fine. If I remove the code I get this error This browser does not support ResizeObserver out of the box. See: https://github.com/react-spring/react-use-measure/#resize-observer-polyfills. Did you remove the polyfill? As with the code above you wouldn't need it. If that is the case maybe is a version issue. I am using react-scripts 4.0.0Peder
Sorry just forgot to mention that the error that I'm getting (if I remove the resize observer mock) comes from a library that I am using in my project (@visx/tooltip). In your case the error might be different because you are using different librariesPeder
I am not using any libraries just a resizeObserver API in a custom hook, as not necessary to use a library. I am using react-scripts 4.0.1 but the error is more a typescript error, not a browser error.Chicanery
Ups sorry, the solution proposed above only works in Javascript. I just found an answer that could help here What about if instead global.ResizeObserver = resizeObserverMock; you add const globalAny:any = global; globalAny.ResizeObserver = resizeObserverMock;. Let me know if that works and I'll edit my answerPeder
M
2

I didn't like any implementation that relies on something similar to an actual implementation. If we are going with integration testing, I'd join RTL's opinion. I needed unit testing which would require isolation. The only way I could think of isolating this is mocking the ResizeObserver. So here is my take on it:

I've created a simple, dumb but externally adressable FakeResizeObserver. The process goes as follows:

  1. Replace window.ResizeObserver with the Fake one
  2. setNextResizeObserverId so that the Fake is addressable
  3. Your actual code creates an instance of your Fake (without knowing it)
  4. You can triggerResizeObserver from the test code

Additionally you have easy access to resizeObserverInstances if that's needed.

// FakeResizeObserver.ts
type ObserverId = string;

let nextObserverId: ObserverId | undefined;

export const setNextResizeObserverId = (observerId: ObserverId) => (nextObserverId = observerId);

const subjects: Record<ObserverId, HTMLElement[]> = {};

export const resizeObserverInstances: Record<ObserverId, FakeResizeObserver> = {};

export const triggerResizeObserver = (observerId: ObserverId, subjects: HTMLElement[]) =>
    resizeObserverInstances[observerId].trigger(subjects);

export default class FakeResizeObserver {
    id: ObserverId;
    callback: (entries: Array<{ target: HTMLElement }>) => unknown;

    constructor(callback: () => unknown) {
        if (typeof nextObserverId === 'undefined') {
            throw new Error(
                'Call setNextResizeObserverId before instantiating a FakeResizeObserver.'
            );
        }

        this.id = nextObserverId;
        nextObserverId = undefined;
        this.callback = callback;
        resizeObserverInstances[this.id] = this;
    }

    trigger(subjects: HTMLElement[]) {
        this.callback(subjects.map((target) => ({ target })));
    }

    observe(element: HTMLElement) {
        if (!subjects[this.id]) {
            subjects[this.id] = [];
        }
        subjects[this.id].push(element);
    }

    unobserve(element: HTMLElement) {
        delete subjects[this.id][subjects[this.id].indexOf(element)];
    }

    disconnect() {
        this.callback = () => undefined;
        delete subjects[this.id];
    }
}

And here is how a test could be implemented:

// useMyHook.test.ts
import React from 'react';
import FakeResizeObserver, {
    setNextResizeObserverId,
    triggerResizeObserver,
} from './FakeResizeObserver';
import useMyHook from './useMyHook';

describe(useMyHook, () => {
    
    // Replace the original observer with the Fake
    let originalResizeObserver: typeof ResizeObserver;
    beforeAll(() => {
        originalResizeObserver = window.ResizeObserver;
        window.ResizeObserver = FakeResizeObserver as any;
    });
    afterAll(() => {
        window.ResizeObserver = originalResizeObserver;
    });

    it('does some stuff', () => {
        // setNextResizeObserverId allows separate instances and access to them.
        setNextResizeObserverId('WHATEVER');

        const element = document.createElement('div');
        const getElementById = jest.spyOn(document, 'getElementById');
        getElementById.mockReturnValue(element);

        // Call the actual hook
        useMyHook();

        // !!! Act as if a change occured
        triggerResizeObserver('WHATEVER', [element]);
        
        // Hope you have something better to test.
        expect(true).toBe(true);
    });
});
Manners answered 10/5, 2023 at 15:50 Comment(0)
G
1

Hey rather than downloading a polyfill you can follow this approach

class ResizeObserver {
  constructor(observerCallback) {
    this.observerCallback = observerCallback;
  }

  observe = () => {
    // using actual dom element as mutation observer requires
    // an actual node in dom
    const scrollContainer = document.querySelector(
      '.horizontal-scroll-view__items',
    );
    // Mutation observer observer any changes in DOM tree
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.type === 'attributes') {
          this.observerCallback();
        }
      });
    });

    observer.observe(scrollContainer, { attributes: true });
  };
}

global.ResizeObserver = ResizeObserver;

With mutation observer you can hardcodingly resize your div and its attributes would be monitored with mutation observer. Hence it will result in callback trigger.

Gusella answered 16/8, 2022 at 12:57 Comment(0)
A
1

I have the same problem in my React project. I solved this problem by following these steps below.

  1. npm i -D resize-observer-polyfill
  2. global.ResizeObserver = require("resize-observer-polyfill"); under the setupTests.ts
Anatola answered 21/12, 2022 at 17:0 Comment(0)
P
0

I implemented the mock in beforeEach so I can test the calls to observer

  let MockObserverInstance: ResizeObserver;

  beforeEach(() => {
    MockObserverInstance = {
      observe: jest.fn(),
      unobserve: jest.fn(),
      disconnect: jest.fn(),
    };
    global.ResizeObserver = jest.fn().mockImplementation(() => MockObserverInstance);
  });

  it('your test', () => {
    ...
    expect(MockObserverInstance.observe).toHaveBeenCalledWith(elem);
  });
Phonate answered 17/5, 2023 at 18:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.