58

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();
    });
});
NiseNise
  • 902
  • 4
  • 15
  • 30

9 Answers9

61

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')
PJRobot
  • 1,306
  • 13
  • 14
  • 3
    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. – Geoff Davids Apr 12 '21 at 01:58
  • 2
    Nice! I had to do `window.ResizeObserver = require...` but then it worked without an issue. – Alex Wally Oct 12 '21 at 21:16
  • 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. – user1383163 Apr 18 '22 at 22:29
  • you made my day and my week!!! – Marc Monserrat Jun 03 '22 at 19:38
  • i had to add `setupFiles: ['./setupTests.ts'],` to `jest.config.js` but then this worked too – Joseph Beuys' Mum Sep 02 '22 at 08:27
  • ...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) – Joseph Beuys' Mum Sep 14 '22 at 10:34
  • One liner helped after lot of efforts. That's was simplest solution. Thanks – Aayushi Gupta Oct 14 '22 at 04:08
  • 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! – Manuel Mariano Silva Feb 23 '23 at 19:20
  • Thanks, quick and simple. – Herick Apr 03 '23 at 23:07
  • 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 } }, [])` – user3051858 Aug 24 '23 at 10:55
48

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

Kailas
  • 7,350
  • 3
  • 47
  • 63
Ron Lavit
  • 1,081
  • 10
  • 9
40

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

Max Yankov
  • 12,551
  • 12
  • 67
  • 135
karel
  • 5,489
  • 46
  • 45
  • 50
  • 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? – Vladyn Feb 16 '21 at 10:25
  • Yeah, I'm still clueless of how to now test any of the logic inside the ResizeObserver :( – ChazUK Mar 03 '22 at 19:28
  • I had to do this as my test says that it does not have `jest` – Rafael Mora Jun 02 '23 at 20:40
14

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.

Joseph King
  • 5,089
  • 1
  • 30
  • 37
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

Silveste
  • 189
  • 2
  • 11
  • 1
    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'``` – NiseNise Jan 04 '21 at 11:28
  • 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.0 – Silveste Jan 05 '21 at 11:47
  • 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 libraries – Silveste Jan 05 '21 at 14:25
  • 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. – NiseNise Jan 07 '21 at 16:09
  • 1
    Ups sorry, the solution proposed above only works in Javascript. I just found an answer that could help [here](https://stackoverflow.com/questions/40743131/how-to-prevent-property-does-not-exist-on-type-global-with-jsdom-and-t) 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 answer – Silveste Jan 11 '21 at 15:14
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
Günay Gültekin
  • 4,486
  • 8
  • 33
  • 36
1

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);
    });
});
Valery
  • 715
  • 6
  • 19
0

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.

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);
  });
Alex
  • 9,250
  • 11
  • 70
  • 81