IntersectionObserver мокирование

В современном фронтенд-разработке часто используются API браузера, которые взаимодействуют с реальной средой, например, IntersectionObserver. Этот API позволяет отслеживать видимость элементов на экране и широко применяется для ленивой подгрузки контента, анимаций при скролле и infinite scroll. В тестах React компонентов прямое использование IntersectionObserver приводит к проблемам: Jest и среда JSDOM не реализуют полноценный браузерный API для наблюдения за пересечениями элементов, поэтому тесты без мокирования будут падать.


Основные принципы мокирования IntersectionObserver

1. Создание глобального мока

Для стабильного тестирования необходимо заменить стандартный IntersectionObserver на мок, который имитирует поведение API без реального взаимодействия с DOM.

Пример базового мока:

beforeAll(() => {
  class IntersectionObserverMock {
    constructor(callback, options) {
      this.callback = callback;
      this.options = options;
    }
    observe = jest.fn();
    unobserve = jest.fn();
    disconnect = jest.fn();
    takeRecords = jest.fn();
  }

  Object.defineProperty(window, 'IntersectionObserver', {
    writable: true,
    configurable: true,
    value: IntersectionObserverMock
  });
});

Ключевые моменты:

  • Методы observe, unobserve, disconnect заменены на jest.fn(), чтобы можно было проверять их вызовы.
  • Конструктор принимает callback и options, имитируя настоящий API.

2. Контроль срабатывания коллбэка

Для проверки логики компонентов важно имитировать срабатывание события пересечения. Это достигается вызовом коллбэка мок-объекта:

function triggerIntersection(entries) {
  window.IntersectionObserver.mock.instances.forEach(instance => {
    instance.callback(entries);
  });
}

Пример использования в тесте:

import { render, screen } from '@testing-library/react';
import LazyImage from './LazyImage';

test('загружает изображение при пересечении', () => {
  render(<LazyImage src="image.jpg" alt="demo" />);

  const img = screen.getByAltText('demo');
  expect(img.src).toContain('placeholder');

  triggerIntersection([{ isIntersecting: true, target: img }]);
  expect(img.src).toContain('image.jpg');
});

Ключевой момент: isIntersecting определяет видимость элемента. Можно передавать массив с разными элементами для тестирования нескольких наблюдаемых объектов.


Продвинутые техники мокирования

1. Настройка разных состояний пересечения

Для сложных компонентов с несколькими точками срабатывания можно использовать кастомные фабрики моков:

function createIntersectionObserverMock(callback) {
  return {
    observe: jest.fn(),
    unobserve: jest.fn(),
    disconnect: jest.fn(),
    callback
  };
}

beforeEach(() => {
  const instances = [];
  window.IntersectionObserver = jest.fn((callback) => {
    const instance = createIntersectionObserverMock(callback);
    instances.push(instance);
    return instance;
  });
  window.IntersectionObserver.instances = instances;
});

Позволяет в тестах получать конкретные экземпляры и манипулировать их состоянием:

const instance = window.IntersectionObserver.instances[0];
instance.callback([{ isIntersecting: true, target: element }]);

2. Использование асинхронного поведения

Для компонентов с асинхронной загрузкой часто нужно дождаться эффекта пересечения:

import { waitFor } from '@testing-library/react';

await waitFor(() => {
  expect(img.src).toContain('image.jpg');
});

Это гарантирует, что компонент успел обработать событие пересечения и обновить состояние.


Частые ошибки при тестировании с IntersectionObserver

  1. Игнорирование мокирования – без замены реального API тесты падают с ошибкой IntersectionObserver is not defined.
  2. Прямое использование useEffect без моков – асинхронные эффекты могут не выполниться, если коллбэк не вызван вручную.
  3. Не проверяется метод disconnect – для компонентов, которые снимают наблюдение, важно убедиться, что ресурсы очищаются.

Пример проверки вызова disconnect:

import { unmount } from '@testing-library/react';

const instance = window.IntersectionObserver.instances[0];
unmount();
expect(instance.disconnect).toHaveBeenCalled();

Рекомендации по структуре тестов с IntersectionObserver

  • Мокировать IntersectionObserver глобально в setupTests.js или в beforeAll.
  • Использовать вспомогательные функции для вызова коллбэка (triggerIntersection).
  • Проверять не только видимость элемента, но и вызовы методов observe и disconnect.
  • При необходимости эмулировать разные сценарии видимости: частичная видимость, полная видимость, уход за пределы viewport.

Эта методика позволяет создавать надежные и предсказуемые тесты React компонентов, использующих IntersectionObserver, обеспечивая контроль над всеми состояниями видимости элементов и предотвращая зависимость от реального DOM браузера.