Взаимодействие в Mapbox GL JS строится вокруг событий карты, слоёв, источников данных и пользовательских жестов. Основная сложность тестирования заключается в том, что библиотека опирается на WebGL, асинхронную отрисовку и внутренний event loop рендерера, что делает традиционные unit-тесты недостаточными без дополнительной инфраструктуры.
Mapbox GL JS оперирует несколькими слоями взаимодействия:
click, mousemove,
mousedown, mouseup, drag)mouseenter, mouseleave,
click по слоям)GeoJSONSource,
VectorSource)render, idle,
data)Ключевой момент: большинство интерактивных событий не являются DOM-событиями в классическом смысле. Они преобразуются через WebGL hit-testing и внутреннюю математику координат.
Это приводит к необходимости тестировать не только обработчики событий, но и состояние карты, источников и слоёв после взаимодействия.
Стандартные тестовые среды (Jest, Vitest) используют
jsdom, который не поддерживает WebGL. Это означает, что
прямое создание карты:
new mapboxgl.Map({ container: div, style: 'mapbox://styles/...' })
вызовет ошибки или не будет рендерить слои.
Используются несколько подходов:
Наиболее распространённый подход — замена библиотеки на mock:
jest.mock('mapbox-gl', () => ({
Map: class {
on() {}
off() {}
addSource() {}
addLayer() {}
removeLayer() {}
removeSource() {}
getLayer() { return {}; }
getSource() { return {}; }
project(lngLat) {
return { x: lngLat.lng * 10, y: lngLat.lat * 10 };
}
}
}));
Этот подход позволяет тестировать бизнес-логику взаимодействия, но не реальную отрисовку.
Более приближённый к реальности вариант — использование WebGL polyfill:
headless-glnode-canvasjsdom + WebGL context stubПример:
import { createCanvas } from 'canvas';
import gl from 'gl';
global.document = {};
global.window = {};
global.HTMLCanvasElement = createCanvas;
const canvas = createCanvas(512, 512);
const context = gl(512, 512, { preserveDrawingBuffer: true });
Этот подход позволяет Mapbox GL JS реально инициализироваться, но остаётся нестабильным для сложных сцен.
Ключевая задача — проверить, что обработчик получает корректные координаты и состояние слоёв обновляется.
map.on('click', 'cities-layer', (e) => {
setSelectedFeature(e.features[0]);
});
Тестирование логики:
test('selects feature on click', () => {
const map = createMockMap();
const handler = jest.fn();
map.on('click', 'cities-layer', handler);
map.trigger('click', {
features: [{ id: 1, properties: { name: 'City' } }]
});
expect(handler).toHaveBeenCalled();
});
Здесь используется искусственный метод trigger,
добавляемый в mock-объект карты.
Hover-события важны для UI-интеракций:
map.on('mouseenter', 'points-layer', () => {
map.getCanvas().style.cursor = 'pointer';
});
Тест:
test('changes cursor on hover', () => {
const map = createMockMap();
const canvas = map.getCanvas();
map.trigger('mouseenter', {});
expect(canvas.style.cursor).toBe('pointer');
});
map.addSource('points', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: []
}
});
Проверка:
test('adds geojson source', () => {
const map = createMockMap();
map.addSource('points', { type: 'geojson', data: {} });
expect(map.getSource('points')).toBeDefined();
});
const source = map.getSource('points');
source.setData(newData);
Тестирование важно для динамических приложений (например, real-time трекинг):
test('updates source data', () => {
const map = createMockMap();
const source = createMockSource();
map.getSource = () => source;
const data = { type: 'FeatureCollection', features: [] };
source.setData = jest.fn();
source.setData(data);
expect(source.setData).toHaveBeenCalledWith(data);
});
Popup в Mapbox GL JS часто используется для отображения информации:
const popup = new mapboxgl.Popup()
.setLngLat([30, 50])
.setHTML('<div>Info</div>')
.addTo(map);
Тестирование обычно ограничивается проверкой вызова методов:
test('creates popup with content', () => {
const popup = createMockPopup();
popup.setHTML('<div>Info</div>');
expect(popup.setHTML).toHaveBeenCalledWith('<div>Info</div>');
});
Unit-тесты недостаточны для проверки поведения карты как системы. Интеграционные тесты чаще всего реализуются через:
test('click selects feature on map', async ({ page }) => {
await page.goto('http://localhost:3000');
await page.click('.mapboxgl-canvas');
const selected = await page.evaluate(() =>
window.__selectedFeature
);
expect(selected).toBeDefined();
});
Mapbox GL JS в таком случае работает в реальном браузере с WebGL, что делает тесты более надёжными.
Mapbox GL JS использует собственную систему координат, поэтому прямые DOM-события недостаточны.
Для тестирования часто создаются утилиты:
function clickMap(map, lng, lat) {
map.fire('click', {
lngLat: { lng, lat },
point: map.project({ lng, lat })
});
}
Это позволяет тестировать бизнес-логику без реального рендеринга.
map.setFilter('cities-layer', ['==', ['get', 'type'], 'city']);
Проверка:
test('applies filter to layer', () => {
const map = createMockMap();
map.setFilter('cities-layer', ['==', 'type', 'city']);
expect(map.setFilter).toHaveBeenCalled();
});
Mapbox GL JS работает асинхронно, поэтому критично учитывать события:
loadrenderidlemap.on('load', () => {
map.addLayer(layer);
});
Тест:
test('adds layer after load', () => {
const map = createMockMap();
const handler = jest.fn();
map.on('load', handler);
map.trigger('load');
expect(handler).toHaveBeenCalled();
});
Перемещение карты связано с состоянием камеры:
map.on('move', () => {
updateViewport(map.getCenter());
});
Проверка:
test('updates viewport on move', () => {
const map = createMockMap();
map.getCenter = () => ({ lng: 10, lat: 20 });
const spy = jest.fn();
map.on('move', () => spy(map.getCenter()));
map.trigger('move');
expect(spy).toHaveBeenCalledWith({ lng: 10, lat: 20 });
});
Практика разделения логики:
Пример:
function handleMapClick(feature, dispatch) {
dispatch({ type: 'SELECT_FEATURE', payload: feature });
}
Тестирование без Mapbox:
test('dispatches selected feature', () => {
const dispatch = jest.fn();
handleMapClick({ id: 1 }, dispatch);
expect(dispatch).toHaveBeenCalledWith({
type: 'SELECT_FEATURE',
payload: { id: 1 }
});
});
Mapbox GL JS активно использует requestAnimationFrame,
что влияет на тесты.
Паттерн стабилизации:
beforeEach(() => {
jest.useFakeTimers();
});
test('renders frame', () => {
const map = createMockMap();
map.trigger('render');
jest.runAllTimers();
expect(true).toBe(true);
});
fire /
triggerСтабильная стратегия обычно включает: