При тестировании приложений, использующих интерактивные карты, ключевая сложность заключается в зависимости от WebGL, DOM API и асинхронных ресурсов рендеринга. Библиотека Mapbox GL JS активно использует графический контекст, Web Workers, тайлы и события пользовательского взаимодействия, что делает её неподходящей для «из коробки» запуска в Node.js тестовой среде.
Для решения этой проблемы тестовая инфраструктура на базе Jest требует комплексной конфигурации: эмуляции браузерного окружения, подмены WebGL, мокирования карты и отключения реальных сетевых запросов.
Первый слой настройки связан с выбором окружения выполнения тестов.
Для 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 отсутствует.
Mapbox GL JS требует WebGLRenderingContext. В Node.js и
jsdom его нет, поэтому тесты падают с ошибкой инициализации карты.
Решение — мокирование canvas и WebGL.
npm install --save-dev jest-canvas-mock
// jest.setup.js
require("jest-canvas-mock");
Эта библиотека эмулирует:
В большинстве 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.mock("mapbox-gl");
Такой подход полностью отключает реальный рендеринг и оставляет только поведенческую модель API.
При работе с Mapbox GL JS важно разделять:
Используют мок mapbox-gl и проверяют:
Иногда требуется проверить:
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()
}))
};
});
Mapbox GL JS использует ряд браузерных API, которых нет в jsdom.
global.IntersectionObserver = class {
constructor() {}
observe() {}
unobserve() {}
disconnect() {}
};
global.ResizeObserver = class {
constructor(callback) {
this.callback = callback;
}
observe() {
this.callback([{ contentRect: { width: 800, height: 600 } }]);
}
unobserve() {}
disconnect() {}
};
global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
global.cancelAnimationFrame = clearTimeout;
Если проект использует современный JavaScript или TypeScript, требуется трансформация.
// babel.config.js
module.exports = {
presets: [
["@babel/preset-env", { targets: { node: "current" } }]
]
};
Если используется 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 загружает стили и тайлы по сети. В тестах это необходимо блокировать.
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({})
})
);
global.fetch = jest.fn(() => {
throw new Error("Network disabled in tests");
});
Mapbox GL JS может импортировать бинарные или worker-модули.
moduleNameMapper: {
"\\.(css|less)$": "identity-obj-proxy",
"\\.worker\\.js$": "<rootDir>/__mocks__/workerMock.js"
}
module.exports = {};
Mapbox использует Web Workers для рендеринга тайлов. В тестах это отключается:
global.Worker = class {
constructor() {}
postMessage() {}
terminate() {}
addEventListener() {}
};
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
Причина: отсутствие canvas mock Решение:
jest-canvas-mock
Причина: неправильный testEnvironment Решение: jsdom
Причина: Mapbox worker API Решение: глобальный mock Worker
Причина: реальные таймеры Mapbox Решение:
jest.useFakeTimers()
Для упрощения тестирования Mapbox рекомендуется отделять:
export function createMap(container) {
return new mapboxgl.Map({
container,
style: "mapbox://styles/base"
});
}
Такой подход позволяет тестировать логику без инициализации карты.
Mapbox использует события:
clickmovezoomloadconst map = new mapboxgl.Map();
map.on.mockImplementation((event, cb) => {
if (event === "click") {
cb({ lngLat: { lng: 10, lat: 20 } });
}
});
При большом количестве тестов Mapbox-стек может замедлять выполнение.
Используются подходы:
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();