Mocking the IntersectionObserver in Jest for testing React components

The IntersectionObserver is a powerful API for tracking elements' visibility within the viewport. Many modern web applications use it to implement lazy loading, infinite scrolling, or other interactions that depend on an element's visibility.

When it comes to testing React components that rely on IntersectionObserver, mocking this API is essential to ensure your tests are reliable and predictable since as of today jest DOM doesn't provide an implementation for it.

In this blog post, you will learn how to mock this API to test React components effectively.

Why Mock IntersectionObserver?

Mocking IntersectionObserver is crucial for several reasons:

  1. Predictable Testing: Mocking ensures that your tests aren't dependent on the user's actual scrolling behavior, making them predictable and consistent.

  2. Fast Testing: Testing real interactions can be slow, especially if you have complex intersection logic. Mocking allows you to run tests quickly without needing to scroll or interact with the page.

  3. Isolation: By isolating your component from external dependencies, you can focus on testing the component's specific behavior. For unit testing, you just want to test the code that is triggered on intersecting, not the actual browser API.

Mocking IntersectionObserver

To mock IntersectionObserver in Jest, we can use a helper function that creates a mock instance of the IntersectionObserver and allows us to control its behavior during testing. Here's an example:

// utils/testing/mockIntersectionObserver.ts

export function mockIntersectionObserver(isIntersectingItems?: Array<boolean>):
[jest.MockedObject<IntersectionObserver>, jest.MockedFn<any>] {
    const intersectionObserverInstanceMock: any = {
        root: null,
        rootMargin: '',
        thresholds: [0],
        observe: jest.fn(),
        unobserve: jest.fn(),
        disconnect: jest.fn(),
        takeRecords: jest.fn(),
    };

    window.IntersectionObserver = jest.fn()
        .mockImplementation(
            (callback: (entries: Array<IntersectionObserverEntry>
            ) => void) => {
                if (isIntersectingItems === undefined) {
                    callback([]);

                    return intersectionObserverInstanceMock;
                }

                const rect = {top: 0, left: 0, bottom: 0, right: 0, x: 0, y: 0, width: 0, height: 0, toJSON: () => ''};
                callback(isIntersectingItems.map((isIntersecting) => ({
                    isIntersecting,
                    intersectionRatio: 0,
                    intersectionRect: rect,
                    rootBounds: rect,
                    boundingClientRect: rect,
                    target: document.createElement('div'),
                    time: 0,
                })));

                return intersectionObserverInstanceMock;
            },
        );

    return [intersectionObserverInstanceMock, window.IntersectionObserver as jest.MockedFn<any>];
}

This helper accepts a list of booleans (which represents the entries and whether they're intersected or not) and returns an array with 2 elements:

  • an IntersectionObserver object mock

  • a constructor function mock (that you can use to make argument-based assertions).

Here's an example of how to use it in a test:

import React from 'react';
import { render, screen } from '@testing-library/react';
import MyComponent from './MyComponent'; // Replace with your component file.
import { mockIntersectionObserver } from '@/utils/testing/mockIntersectionObserver.ts'

describe('MyComponent', () => {
    it('should render when intersecting', () => {
        const onIntersect = jest.fn();
        const [intersectionObserver] = mockIntersectionObserver([true]);
        render(<MyComponent onIntersect={onIntersect} />);
        const element = screen.getByText('Visible Content');

        expect(element).toBeInTheDocument();
        expect(intersectionObserver.observe).toHaveBeenCalledTimes(1);
        expect(onIntersect).toHaveBeenCalledTimes(1);
        // (other assertions...)
    });

    it('should not render when not intersecting', () => {
        const onIntersect = jest.fn();
        const [intersectionObserver] = mockIntersectionObserver([false]);
        render(<MyComponent onIntersect={onIntersect} />);
        const element = screen.queryByText('Hidden Content');

        expect(element).toBeNull();
        expect(intersectionObserver.observe).toHaveBeenCalledTimes(1);
        expect(onIntersect).not.toHaveBeenCalled();
        // (other assertions...)
    });
});

This approach allows you to thoroughly test your components' visibility logic without relying on real user interactions.

Happy testing!

Did you find this article valuable?

Support Javier Aguilar by becoming a sponsor. Any amount is appreciated!