window.matchMedia мокирование

При тестировании React-компонентов часто возникает необходимость работать с медиа-запросами, особенно когда используются CSS-фреймворки или библиотека styled-components с условными стилями в зависимости от размеров экрана. В браузерах для работы с медиа-запросами используется метод window.matchMedia, который возвращает объект с информацией о соответствии текущего окна указанному медиавыражению.

В среде тестирования, такой как Jest с React Testing Library, реальный браузерный объект window.matchMedia отсутствует, поэтому необходимо его мокировать, чтобы тесты корректно работали и можно было проверять поведение компонентов при разных разрешениях экрана.


Структура объекта, возвращаемого window.matchMedia

Метод window.matchMedia(query) возвращает объект со следующими ключевыми свойствами и методами:

  • matches — булево значение, указывающее, удовлетворяет ли текущее окно медиавыражению.
  • media — строка с самим медиавыражением.
  • addListener — функция, добавляющая слушатель на изменение соответствия медиавыражению (устаревшая, но часто используется).
  • removeListener — функция, удаляющая слушатель (устаревшая).
  • addEventListener — современный аналог addListener.
  • removeEventListener — современный аналог removeListener.
  • dispatchEvent — метод для ручного вызова события.

Для тестирования достаточно базовой реализации с matches, addListener и removeListener, чтобы React-компоненты могли корректно реагировать на медиа-запросы.


Простое мокирование window.matchMedia в Jest

Наиболее простой способ — переопределить window.matchMedia перед тестами:

beforeAll(() => {
  Object.defineProperty(window, 'matchMedia', {
    writable: true,
    value: jest.fn().mockImplementation(query => ({
      matches: false,
      media: query,
      onchange: null,
      addListener: jest.fn(), // устаревший метод
      removeListener: jest.fn(), // устаревший метод
      addEventListener: jest.fn(), // современный метод
      removeEventListener: jest.fn(), // современный метод
      dispatchEvent: jest.fn(),
    })),
  });
});

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

  • jest.fn() позволяет отслеживать вызовы методов.
  • matches можно менять внутри теста, чтобы симулировать разные размеры экрана.
  • Переопределение через Object.defineProperty необходимо, так как window.matchMedia может быть непереопределяемым по умолчанию.

Мокирование с изменением состояния медиа-запроса

Для тестов, где важно проверять адаптивное поведение компонента, необходимо уметь менять значение matches во время выполнения теста:

const createMatchMedia = (matches) => () => ({
  matches,
  media: '',
  onchange: null,
  addListener: jest.fn(),
  removeListener: jest.fn(),
  addEventListener: jest.fn(),
  removeEventListener: jest.fn(),
  dispatchEvent: jest.fn(),
});

test('компонент рендерится по медиазапросу', () => {
  window.matchMedia = createMatchMedia(true);
  
  render(<MyComponent />);
  
  expect(screen.getByText(/адаптивный контент/i)).toBeInTheDocument();
  
  // Меняем на false для симуляции меньшего экрана
  window.matchMedia = createMatchMedia(false);
});

Этот подход позволяет эмулировать разные состояния окна и проверять корректное отображение компонентов.


Интеграция с React Hooks (useMediaQuery)

Многие проекты используют кастомные хуки для работы с медиавыражениями, например:

function useMediaQuery(query) {
  const [matches, setMatches] = useState(window.matchMedia(query).matches);

  useEffect(() => {
    const mediaQuery = window.matchMedia(query);
    const handler = (event) => setMatches(event.matches);

    mediaQuery.addEventListener('change', handler);
    return () => mediaQuery.removeEventListener('change', handler);
  }, [query]);

  return matches;
}

Для тестирования такого хука мок window.matchMedia должен корректно вызывать addEventListener и removeEventListener, чтобы эффект корректно подписывался и снимался.

test('хук корректно реагирует на изменение медиазапроса', () => {
  const changeEvent = { matches: true };
  
  const listeners = [];
  window.matchMedia = jest.fn().mockImplementation(() => ({
    matches: false,
    addEventListener: (event, handler) => listeners.push(handler),
    removeEventListener: jest.fn(),
    media: '',
    onchange: null,
    dispatchEvent: jest.fn(),
  }));

  const { result } = renderHook(() => useMediaQuery('(min-width: 768px)'));
  
  expect(result.current).toBe(false);
  
  act(() => {
    listeners.forEach(handler => handler(changeEvent));
  });

  expect(result.current).toBe(true);
});

Подход с глобальным моком для всех тестов

Если проект содержит много тестов, удобно создать глобальный мок в файле setupTests.js:

global.matchMedia = global.matchMedia || jest.fn().mockImplementation(query => ({
  matches: false,
  media: query,
  onchange: null,
  addListener: jest.fn(),
  removeListener: jest.fn(),
  addEventListener: jest.fn(),
  removeEventListener: jest.fn(),
  dispatchEvent: jest.fn(),
}));

Это позволяет не дублировать мок в каждом тесте, а при необходимости изменять matches прямо внутри конкретного теста.


Рекомендации по использованию

  • Всегда определять все ключевые методы (addListener, removeListener, addEventListener, removeEventListener), иначе компоненты, использующие события изменения медиазапроса, будут ломаться в тестах.
  • Для тестов адаптивного дизайна менять значение matches динамически через фабрики моков.
  • Использовать глобальный мок для проекта, чтобы снизить дублирование и обеспечить единообразие тестовой среды.
  • При необходимости проверять вызовы обработчиков можно использовать jest.fn() и expect для отслеживания вызовов.

Мокирование window.matchMedia — ключевой шаг для полноценного тестирования адаптивных компонентов, особенно в сочетании с кастомными хуками и библиотеками UI, которые опираются на медиазапросы.