Мокирование PerformanceObserver в Jest и Vitest

Web Vitals — это набор метрик, которые помогают измерять качество пользовательского опыта на веб-странице. Ключевыми показателями являются Largest Contentful Paint (LCP), First Input Delay (FID) и Cumulative Layout Shift (CLS). В JavaScript доступ к этим метрикам реализован через API PerformanceObserver, который позволяет отслеживать различные типы событий производительности в реальном времени.

Объект PerformanceObserver создается с помощью конструктора и принимает callback-функцию, которая вызывается при появлении новых записей производительности:

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    console.log(entry.name, entry.startTime, entry.duration);
  });
});

observer.observe({ type: 'largest-contentful-paint', buffered: true });

Параметр buffered: true позволяет получать все метрики, которые уже произошли до подключения наблюдателя, что особенно важно при измерении LCP.


Мокирование PerformanceObserver в тестах

Для юнит-тестирования компонентов, которые используют PerformanceObserver, необходимо мокировать его поведение, чтобы избежать зависимости от реального браузерного окружения и асинхронных событий. В Jest и Vitest это делается через замещение глобального объекта PerformanceObserver.

Простейший пример мокирования выглядит так:

class MockPerformanceObserver {
  constructor(callback) {
    this.callback = callback;
    this.entries = [];
  }

  observe({ type, buffered }) {
    if (buffered) {
      this.callback({
        getEntries: () => this.entries,
      });
    }
  }

  disconnect() {}

  trigger(entry) {
    this.entries.push(entry);
    this.callback({
      getEntries: () => [entry],
    });
  }
}

global.PerformanceObserver = MockPerformanceObserver;

Метод trigger позволяет вручную отправлять записи в тестируемый код, эмулируя реальные события.


Тестирование LCP

Для проверки правильной обработки LCP в компоненте можно написать тест:

test('обрабатывает LCP', () => {
  const lcpEntry = { name: 'largest-contentful-paint', startTime: 1234, duration: 0 };
  const observer = new PerformanceObserver((list) => {
    const entry = list.getEntries()[0];
    expect(entry.name).toBe('largest-contentful-paint');
    expect(entry.startTime).toBe(1234);
  });

  observer.observe({ type: 'largest-contentful-paint', buffered: true });
  observer.trigger(lcpEntry);
});

В этом примере мы создаем тестовую запись LCP и вручную вызываем callback через метод trigger, проверяя корректность обработки данных.


Тестирование FID и CLS

FID измеряет задержку между взаимодействием пользователя и обработкой события. CLS — смещение макета страницы. Для них логика мокирования аналогична LCP:

const fidEntry = { name: 'first-input', startTime: 2000, processingStart: 2002 };
const clsEntry = { name: 'layout-shift', startTime: 3000, value: 0.1 };

observer.trigger(fidEntry);
observer.trigger(clsEntry);

В тестах проверяются свойства processingStart у FID и value у CLS. Это позволяет гарантировать, что компонент правильно реагирует на метрики производительности.


Использование Jest Spy для контроля вызовов

Для более детальной проверки можно комбинировать мок PerformanceObserver с Jest Spy:

const callbackSpy = jest.fn();
const observer = new PerformanceObserver(callbackSpy);

observer.observe({ type: 'largest-contentful-paint', buffered: true });
observer.trigger({ name: 'largest-contentful-paint', startTime: 1000 });

expect(callbackSpy).toHaveBeenCalledTimes(1);
expect(callbackSpy.mock.calls[0][0].getEntries()[0].startTime).toBe(1000);

Это позволяет точно отследить количество вызовов callback и содержимое передаваемых записей.


Особенности мокирования в Vitest

Vitest полностью совместим с Jest-подобным синтаксисом мокирования, включая vi.fn() для spy и vi.stubGlobal() для глобальных объектов:

vi.stubGlobal('PerformanceObserver', MockPerformanceObserver);
const callback = vi.fn();
const observer = new PerformanceObserver(callback);

observer.observe({ type: 'largest-contentful-paint', buffered: true });
observer.trigger({ name: 'largest-contentful-paint', startTime: 500 });

expect(callback).toHaveBeenCalled();

Метод vi.stubGlobal позволяет заменить PerformanceObserver на мок-версию только в тестовой среде, не затрагивая глобальные объекты при реальном запуске кода.


Рекомендации по организации тестов

  1. Изоляция моков: всегда сбрасывать глобальные замещения после теста (afterEach(() => { vi.restoreAllMocks(); })).
  2. Использование buffered: для метрик типа LCP и CLS важно получать уже накопленные записи.
  3. Явный вызов trigger: тесты должны управлять временем событий, чтобы избежать нестабильности.
  4. Проверка всех полей записи: метрики Web Vitals содержат несколько свойств, которые критичны для анализа.

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