89

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

10 Answers 10

105

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

Sign up to request clarification or add additional context in comments.

4 Comments

That worked for me in stencil js with jest testing. Thanks
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 <3
replacing global with window worked for me.
This worked with Vitest by changing jest.fn to vi.fn.
70

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

9 Comments

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.
Nice! I had to do window.ResizeObserver = require... but then it worked without an issue.
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.
you made my day and my week!!!
i had to add setupFiles: ['./setupTests.ts'], to jest.config.js but then this worked too
|
44

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

3 Comments

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?
Yeah, I'm still clueless of how to now test any of the logic inside the ResizeObserver :(
I had to do this as my test says that it does not have jest
18

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.

Comments

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

5 Comments

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'
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
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
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.
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 answer
2

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.

Comments

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

Comments

1

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

Comments

0

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

Comments

0

We had similar issues in our environment. The cleanest solution in our vitest environment - add code to test-setup.ts as follows:

const ResizeObserverMock = class {
  observe(): void {
    // void
  }
  unobserve(): void {
    // void
  }
  disconnect(): void {
    // void
  }
};

vi.stubGlobal('ResizeObserver', ResizeObserverMock);

We then moved our individual mocks directly into the component's spec file itself:

vi.mock('@myLib/o-data', async importOriginal => {
    const actual = await importOriginal();
 ...
}

Also see https://vitest.dev/guide/mocking/globals.html

and my full post How to vi.mock the ResizeObserver used within our custom ResizeObserver Directive

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.