При тестировании 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);
});
Этот подход позволяет эмулировать разные состояния окна и проверять корректное отображение компонентов.
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, которые опираются на
медиазапросы.