Моки и стабы для DOM

В контексте JavaScript и работы с библиотекой Floating UI, моки и стабы используются для имитации поведения элементов DOM и API браузера, чтобы тестировать позиционирование всплывающих элементов без необходимости реального рендеринга в браузере. Мок (mock) — это объект, который имитирует поведение настоящего объекта, включая методы и их возвращаемые значения. Стаб (stub) — частный случай мока, обычно подменяющий конкретную функцию или метод для контроля её результата.

Отличие между моками и стабами

  • Стабы заменяют отдельные функции или методы, фиксируя их поведение на заранее заданные результаты.
  • Моки позволяют не только подменять функции, но и отслеживать вызовы, аргументы и порядок вызова, что особенно важно для тестирования комплексной логики Floating UI.

Использование document.createElement и подмена размеров

Floating UI активно использует размеры и расположение элементов через свойства DOM, такие как offsetWidth, offsetHeight, getBoundingClientRect. В тестах необходимо создавать стабы для этих свойств:

const reference = document.createElement('div');
Object.defineProperty(reference, 'offsetWidth', { value: 100 });
Object.defineProperty(reference, 'offsetHeight', { value: 50 });

const floating = document.createElement('div');
Object.defineProperty(floating, 'offsetWidth', { value: 80 });
Object.defineProperty(floating, 'offsetHeight', { value: 40 });

При этом getBoundingClientRect можно замокать, чтобы возвращать контролируемые координаты:

reference.getBoundingClientRect = () => ({
  width: 100,
  height: 50,
  top: 100,
  left: 200,
  right: 300,
  bottom: 150,
});

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


Моки для API окна и скролла

Для корректного тестирования поведения Floating UI на странице с прокруткой необходимо мокировать свойства window и document.documentElement:

Object.defineProperty(window, 'scrollX', { value: 50 });
Object.defineProperty(window, 'scrollY', { value: 100 });

Object.defineProperty(document.documentElement, 'clientWidth', { value: 1024 });
Object.defineProperty(document.documentElement, 'clientHeight', { value: 768 });

Такой подход обеспечивает предсказуемое поведение алгоритмов flip, shift и offset, применяемых библиотекой для динамического позиционирования.


Использование Jest и Sinon для моков и стабов

Библиотеки Jest и Sinon позволяют создавать сложные моки DOM API с минимальными усилиями. Пример подмены метода с Jest:

const getBoundingClientRectMock = jest.fn(() => ({
  width: 120,
  height: 60,
  top: 150,
  left: 250,
  right: 370,
  bottom: 210,
}));

const reference = document.createElement('div');
reference.getBoundingClientRect = getBoundingClientRectMock;

С Sinon можно дополнительно проверять, сколько раз вызвана функция, с какими аргументами:

const stub = sinon.stub(reference, 'getBoundingClientRect').returns({
  width: 120,
  height: 60,
  top: 150,
  left: 250,
});

Такой контроль позволяет симулировать разные сценарии позиционирования и тестировать edge-cases, которые сложно воспроизвести вручную.


Моки событий и взаимодействий

Floating UI реагирует на события scroll, resize, pointermove. Для тестов создаются стабы для событий:

const event = new Event('scroll');
window.dispatchEvent(event);

const resizeEvent = new Event('resize');
window.dispatchEvent(resizeEvent);

Можно также мокать слушатели событий, чтобы перехватывать их вызовы без фактического изменения DOM:

const addEventListenerStub = jest.spyOn(window, 'addEventListener');

Это позволяет проверять, корректно ли Floating UI подписывается на события и вызывает соответствующие колбэки.


Интеграция моков и стабов с Floating UI

Для полноценного тестирования компонентов с Floating UI рекомендуется создавать контролируемую виртуальную среду:

  1. Создать фиктивные элементы reference и floating с предсказуемыми размерами.
  2. Замокать getBoundingClientRect и свойства offsetWidth, offsetHeight.
  3. Подменить window.scrollX/Y и размеры viewport для тестирования смещений.
  4. Мокировать события scroll/resize для проверки реактивного позиционирования.
  5. Использовать Jest или Sinon для отслеживания вызовов методов и проверки логики поведения.

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


Контроль побочных эффектов

Важно следить за тем, чтобы моки и стабы не оставались после теста, иначе они могут повлиять на последующие проверки. Для Jest и Sinon существуют методы очистки:

jest.restoreAllMocks();
sinon.restore();

Это гарантирует, что любые изменения DOM API или методов будут отменены, и среда вернется в исходное состояние.


Практический пример полного теста Floating UI

import { computePosition } from '@floating-ui/dom';

test('проверка позиции тултипа', () => {
  const reference = document.createElement('div');
  Object.defineProperty(reference, 'offsetWidth', { value: 100 });
  Object.defineProperty(reference, 'offsetHeight', { value: 50 });
  reference.getBoundingClientRect = () => ({ width: 100, height: 50, top: 100, left: 200, right: 300, bottom: 150 });

  const floating = document.createElement('div');
  Object.defineProperty(floating, 'offsetWidth', { value: 80 });
  Object.defineProperty(floating, 'offsetHeight', { value: 40 });

  const result = computePosition(reference, floating, { placement: 'top' });
  
  expect(result.x).toBe(210); // пример ожидаемого значения с учетом offset
  expect(result.y).toBe(60);
});

В этом тесте полностью контролируется окружение: размеры элементов, позиции и даже результаты вычислений Floating UI. Такой подход позволяет создавать воспроизводимые тесты для сложных UI-сценариев.