Jest конфигурация

При тестировании приложений, использующих интерактивные карты, ключевая сложность заключается в зависимости от WebGL, DOM API и асинхронных ресурсов рендеринга. Библиотека Mapbox GL JS активно использует графический контекст, Web Workers, тайлы и события пользовательского взаимодействия, что делает её неподходящей для «из коробки» запуска в Node.js тестовой среде.

Для решения этой проблемы тестовая инфраструктура на базе Jest требует комплексной конфигурации: эмуляции браузерного окружения, подмены WebGL, мокирования карты и отключения реальных сетевых запросов.


Базовая конфигурация Jest для фронтенд-проекта

Первый слой настройки связан с выбором окружения выполнения тестов. Для Mapbox-проектов почти всегда используется jsdom, так как он имитирует DOM.

// jest.config.js
module.exports = {
  testEnvironment: "jsdom",
  testMatch: ["**/__tests__/**/*.test.js"],
  transform: {
    "^.+\\.(js|jsx)$": "babel-jest"
  },
  moduleFileExtensions: ["js", "jsx", "json"],
  setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
  clearMocks: true,
  resetMocks: true,
  restoreMocks: true
};

Ключевым параметром является:

  • testEnvironment: "jsdom" — обеспечивает наличие window, document, Element

Однако этого недостаточно для Mapbox GL JS, так как WebGL в jsdom отсутствует.


Проблема WebGL и её обход

Mapbox GL JS требует WebGLRenderingContext. В Node.js и jsdom его нет, поэтому тесты падают с ошибкой инициализации карты.

Решение — мокирование canvas и WebGL.

Установка зависимостей

npm install --save-dev jest-canvas-mock

Подключение моков

// jest.setup.js
require("jest-canvas-mock");

Эта библиотека эмулирует:

  • HTMLCanvasElement
  • CanvasRenderingContext2D
  • базовую WebGL структуру

Мокирование Mapbox GL JS

В большинстве unit-тестов нет необходимости создавать реальную карту. Вместо этого библиотека полностью мокируется.

Базовый мок

// __mocks__/mapbox-gl.js

class Map {
  constructor() {
    this.getCanvas = jest.fn(() => ({
      getContext: jest.fn()
    }));

    this.on = jest.fn();
    this.remove = jest.fn();
    this.addControl = jest.fn();
    this.setStyle = jest.fn();
    this.setCenter = jest.fn();
    this.setZoom = jest.fn();
  }
}

module.exports = {
  Map,
  NavigationControl: jest.fn(),
  Marker: jest.fn(() => ({
    setLngLat: jest.fn().mockReturnThis(),
    addTo: jest.fn().mockReturnThis()
  }))
};

Подключение мока в Jest

jest.mock("mapbox-gl");

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


Разделение unit и integration тестов

При работе с Mapbox GL JS важно разделять:

  • Unit-тесты (без реальной карты)
  • Интеграционные тесты (с ограниченной инициализацией Mapbox)

Unit-тесты

Используют мок mapbox-gl и проверяют:

  • вызовы методов
  • корректность передачи параметров
  • реакцию UI-логики

Интеграционные тесты

Иногда требуется проверить:

  • инициализацию карты
  • работу событий (load, move, click)

Для этого используют частичный мок:

jest.mock("mapbox-gl", () => {
  const original = jest.requireActual("mapbox-gl");

  return {
    ...original,
    Map: jest.fn().mockImplementation(() => ({
      on: (event, cb) => {
        if (event === "load") cb();
      },
      remove: jest.fn()
    }))
  };
});

Эмуляция DOM API, необходимых Mapbox

Mapbox GL JS использует ряд браузерных API, которых нет в jsdom.

IntersectionObserver

global.IntersectionObserver = class {
  constructor() {}
  observe() {}
  unobserve() {}
  disconnect() {}
};

ResizeObserver

global.ResizeObserver = class {
  constructor(callback) {
    this.callback = callback;
  }
  observe() {
    this.callback([{ contentRect: { width: 800, height: 600 } }]);
  }
  unobserve() {}
  disconnect() {}
};

requestAnimationFrame

global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
global.cancelAnimationFrame = clearTimeout;

Настройка Babel для Jest

Если проект использует современный JavaScript или TypeScript, требуется трансформация.

Babel конфигурация

// babel.config.js
module.exports = {
  presets: [
    ["@babel/preset-env", { targets: { node: "current" } }]
  ]
};

Важно при ESM

Если используется ESM (import/export), Jest требует дополнительной настройки:

{
  "type": "module"
}

и

transform: {}

или использование babel-jest с поддержкой ESM.


Работа с асинхронной инициализацией карты

Mapbox GL JS инициализирует карту асинхронно, поэтому тесты должны учитывать события загрузки.

Пример теста

import mapboxgl from "mapbox-gl";

test("map initializes and triggers load event", () => {
  const map = new mapboxgl.Map({
    container: document.createElement("div"),
    style: "mapbox://styles/test/style"
  });

  const loadHandler = jest.fn();
  map.on("load", loadHandler);

  // имитация загрузки
  map.on.mock.calls.forEach(([event, cb]) => {
    if (event === "load") cb();
  });

  expect(loadHandler).toHaveBeenCalled();
});

Изоляция сетевых запросов

Mapbox загружает стили и тайлы по сети. В тестах это необходимо блокировать.

Глобальный мок fetch

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({})
  })
);

Более строгая блокировка

global.fetch = jest.fn(() => {
  throw new Error("Network disabled in tests");
});

Настройка moduleNameMapper

Mapbox GL JS может импортировать бинарные или worker-модули.

moduleNameMapper: {
  "\\.(css|less)$": "identity-obj-proxy",
  "\\.worker\\.js$": "<rootDir>/__mocks__/workerMock.js"
}

worker mock

module.exports = {};

Отключение worker_threads поведения

Mapbox использует Web Workers для рендеринга тайлов. В тестах это отключается:

global.Worker = class {
  constructor() {}
  postMessage() {}
  terminate() {}
  addEventListener() {}
};

Контроль таймеров Jest

Mapbox активно использует таймеры для анимаций и обновления состояния карты.

jest.useFakeTimers();

Пример теста:

test("animation timing", () => {
  const callback = jest.fn();

  setTimeout(callback, 1000);

  jest.advanceTimersByTime(1000);

  expect(callback).toHaveBeenCalled();
});

Структура тестового окружения

Типичная структура проекта:

project/
  src/
    map/
    components/
  __tests__/
  __mocks__/
  jest.config.js
  jest.setup.js
  babel.config.js

Частые ошибки конфигурации

1. Ошибка WebGL context

Причина: отсутствие canvas mock Решение: jest-canvas-mock


2. Map is not defined / window undefined

Причина: неправильный testEnvironment Решение: jsdom


3. Worker is not defined

Причина: Mapbox worker API Решение: глобальный mock Worker


4. Infinite async loops

Причина: реальные таймеры Mapbox Решение: jest.useFakeTimers()


Изоляция логики карты в архитектуре приложения

Для упрощения тестирования Mapbox рекомендуется отделять:

  • создание карты
  • управление состоянием
  • обработку событий
  • бизнес-логику

Пример слоя абстракции

export function createMap(container) {
  return new mapboxgl.Map({
    container,
    style: "mapbox://styles/base"
  });
}

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


Тестирование событий пользовательского взаимодействия

Mapbox использует события:

  • click
  • move
  • zoom
  • load

Мок событий

const map = new mapboxgl.Map();

map.on.mockImplementation((event, cb) => {
  if (event === "click") {
    cb({ lngLat: { lng: 10, lat: 20 } });
  }
});

Оптимизация тестового времени выполнения

При большом количестве тестов Mapbox-стек может замедлять выполнение.

Используются подходы:

  • глобальный мок Mapbox
  • отключение worker
  • fake timers
  • минимизация DOM операций
  • запрет реальных fetch

Итоговая структура jest.setup.js

require("jest-canvas-mock");

global.IntersectionObserver = class {};
global.ResizeObserver = class {
  observe() {}
  unobserve() {}
  disconnect() {}
};

global.Worker = class {
  postMessage() {}
  terminate() {}
};

global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
global.cancelAnimationFrame = clearTimeout;

global.fetch = jest.fn();