Библиотека Leaflet тесно связана с DOM, графическим контекстом и асинхронной загрузкой тайлов, что формирует специфический профиль тестирования. В отличие от чистых вычислительных модулей, здесь значительная часть логики завязана на браузерное окружение, события мыши, геометрические преобразования координат и взаимодействие слоёв карты.
Ключевая сложность заключается в том, что большая часть
функциональности Leaflet опирается на реальные браузерные API:
HTMLElement, Canvas,
requestAnimationFrame, обработчики событий и сетевые
запросы к тайл-серверам. Поэтому юнит-тестирование требует частичной
эмуляции среды выполнения.
При тестировании компонентов Leaflet важно разделять:
Чистая логика может тестироваться напрямую, тогда как DOM-часть требует эмуляции через инструменты вроде JSDOM или jsdom-like окружений.
Пример выделения логики:
function latLngToTile(lat, lng, zoom) {
const n = Math.pow(2, zoom);
const x = Math.floor((lng + 180) / 360 * n);
const y = Math.floor(
(1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * n
);
return { x, y };
}
Такую функцию можно тестировать без Leaflet и без карты:
test('latLngToTile converts coordinates correctly', () => {
const result = latLngToTile(55.75, 37.62, 10);
expect(result).toEqual({ x: expect.any(Number), y: expect.any(Number) });
});
Наиболее распространённая связка для тестирования Leaflet-кода — Jest с JSDOM. JSDOM эмулирует DOM-структуру, позволяя создавать контейнер карты без реального браузера.
Инициализация тестового окружения:
import { JSDOM } from 'jsdom';
const dom = new JSDOM('<!DOCTYPE html><div id="map"></div>');
global.document = dom.window.document;
global.window = dom.window;
Создание карты в тесте:
import L from 'leaflet';
test('map initializes without errors', () => {
const map = L.map('map').setView([51.505, -0.09], 13);
expect(map).toBeDefined();
});
Важно учитывать, что JSDOM не реализует canvas и часть геометрических API, поэтому визуальные операции часто требуют моков.
Сетевые запросы к тайлам являются одной из основных проблем при
тестировании. Реальные HTTP-запросы недопустимы в юнит-тестах, поэтому
используется подмена L.TileLayer.
Пример заглушки:
class MockTileLayer {
constructor() {
this.tiles = {};
}
addTo(map) {
this.map = map;
return this;
}
getTileUrl(coords) {
return `http://example.com/${coords.z}/${coords.x}/${coords.y}.png`;
}
}
Подмена в тесте:
L.TileLayer = MockTileLayer;
test('tile layer generates url', () => {
const layer = new L.TileLayer();
const url = layer.getTileUrl({ x: 1, y: 2, z: 3 });
expect(url).toBe('http://example.com/3/1/2.png');
});
Leaflet активно использует событийную модель: click,
zoom, move, load. Тестирование
этих механизмов требует симуляции DOM-событий.
Пример теста клика:
test('map click event triggers handler', () => {
const map = L.map('map').setView([0, 0], 1);
const handler = jest.fn();
map.on('click', handler);
map.fire('click', {
latlng: L.latLng(10, 10)
});
expect(handler).toHaveBeenCalled();
});
Метод fire позволяет вызывать события без реального
взаимодействия с интерфейсом.
Слои Leaflet представляют собой отдельные сущности, добавляемые и удаляемые с карты. Юнит-тестирование слоёв фокусируется на их жизненном цикле.
test('layer can be added and removed', () => {
const map = L.map('map');
const layer = L.marker([0, 0]);
layer.addTo(map);
expect(map.hasLayer(layer)).toBe(true);
map.removeLayer(layer);
expect(map.hasLayer(layer)).toBe(false);
});
Особое внимание уделяется корректности обновления внутренних коллекций слоёв.
GeoJSON-слои являются одной из наиболее сложных структур из-за вложенности данных и стилей.
test('geojson layer applies style', () => {
const geojson = {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [30, 10]
}
};
const layer = L.geoJSON(geojson, {
pointToLayer: () => L.circleMarker([10, 30])
});
expect(layer.getLayers().length).toBe(1);
});
Проверяется корректность парсинга и генерации внутренних слоёв.
Загрузка тайлов выполняется асинхронно, что требует использования моков промисов или подмены загрузчиков изображений.
Подмена Image:
global.Image = class {
constructor() {
setTimeout(() => {
this.onload && this.onload();
}, 0);
}
};
Тестирование загрузки:
test('tile image loads successfully', async () => {
const layer = L.tileLayer('http://example.com/{z}/{x}/{y}.png');
layer.addTo(L.map('map'));
await new Promise(resolve => setTimeout(resolve, 10));
expect(layer.isLoading).toBeDefined();
});
Контролы Leaflet (zoom, layers, custom buttons) требуют проверки DOM-структуры и событий.
const CustomControl = L.Control.extend({
onAdd: function () {
const div = L.DomUtil.create('div', 'custom-control');
div.innerHTML = 'Test';
return div;
}
});
Тест:
test('custom control renders DOM element', () => {
const map = L.map('map');
const control = new CustomControl();
control.addTo(map);
const el = document.querySelector('.custom-control');
expect(el).not.toBeNull();
});
Leaflet использует проекцию Web Mercator, что требует проверки корректности преобразований координат.
test('projection converts correctly', () => {
const point = L.CRS.EPSG3857.project(L.latLng(0, 0));
expect(point.x).toBe(0);
expect(point.y).toBe(0);
});
При этом важно учитывать погрешности вычислений с плавающей точкой.
Хотя это ближе к интеграционному тестированию, часто проверяется скорость пересчёта координат и обновления слоёв.
test('projection performance under load', () => {
const start = performance.now();
for (let i = 0; i < 10000; i++) {
L.CRS.EPSG3857.project(L.latLng(i % 90, i % 180));
}
const end = performance.now();
expect(end - start).toBeLessThan(200);
});
Leaflet активно использует classList,
appendChild, removeChild. При тестировании
применяются следующие подходы:
L.DomUtildocument.createElementПример:
jest.spyOn(L.DomUtil, 'addClass');
L.DomUtil.addClass(document.createElement('div'), 'test');
expect(L.DomUtil.addClass).toHaveBeenCalled();
Плагины расширяют базовую функциональность карты, добавляя новые контролы, слои и обработчики. Их тестирование обычно строится на проверке интеграции с ядром Leaflet.
test('plugin registers method on map', () => {
L.Map.include({
testPlugin: function () {
return true;
}
});
const map = L.map('map');
expect(map.testPlugin()).toBe(true);
});
Корректное освобождение ресурсов важно для предотвращения утечек памяти.
test('map is properly removed', () => {
const map = L.map('map');
map.remove();
expect(map._container).toBeNull();
});
Особое внимание уделяется очистке событий и таймеров.
Анимации зума и панорамирования требуют контроля времени выполнения.
global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
Это позволяет детерминировать поведение тестов, связанных с анимацией.
Юнит-тестирование включает сценарии некорректных данных:
test('invalid latlng throws or handles gracefully', () => {
expect(() => L.latLng(null, null)).toThrow();
});
Такие тесты фиксируют устойчивость API к ошибкам входных данных.