Тестирование событий

Система событий в MapLibre GL JS построена вокруг модели наблюдателя: карта и её сущности (слои, источники данных, DOM-интеграции) генерируют события, на которые подписываются обработчики через map.on, map.once и снимаются через map.off.

События делятся на несколько уровней:

  • События жизненного цикла карты
  • События взаимодействия пользователя
  • События источников данных
  • События рендеринга
  • События слоёв и стилей

Каждое событие передаёт объект контекста, содержащий состояние карты, координаты, свойства фич и технические параметры рендера.


Типы событий и их особенности

Жизненный цикл карты

Ключевые события, определяющие готовность карты:

  • load — стиль и ресурсы загружены, карта готова к работе
  • style.load — загружен стиль
  • idle — отсутствуют активные операции рендеринга
  • remove — карта уничтожается

Особенность тестирования этих событий заключается в необходимости учитывать асинхронную загрузку ресурсов (тайлы, изображения, glyphs).


Пользовательские события

Основной класс событий взаимодействия:

  • click
  • dblclick
  • mousemove
  • mouseenter / mouseleave
  • mousedown / mouseup
  • touchstart / touchend

Эти события содержат:

  • координаты экрана (point)
  • географические координаты (lngLat)
  • список пересечённых объектов (features)

В тестах важно учитывать, что features формируются только после завершения рендеринга и наличия данных в соответствующих слоях.


События слоёв и источников

  • data — изменение данных источника
  • sourcedata — прогресс загрузки источника
  • dataloading — начало загрузки данных
  • sourcedataloading — начало загрузки конкретного источника
  • source.load — источник полностью загружен

Особенность: эти события часто возникают несколько раз в одном сценарии, что требует фильтрации в тестах.


События рендеринга

  • render — каждый кадр отрисовки
  • rendercomplete — завершение финального рендера

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


Подходы к тестированию событий

Уровни тестирования

Тестирование событий в MapLibre GL JS обычно делится на три уровня:

  1. Юнит-тестирование обработчиков
  2. Интеграционное тестирование карты
  3. E2E тестирование взаимодействия

Каждый уровень требует разных инструментов и допущений.


Юнит-тестирование обработчиков событий

Юнит-тестирование применяется для проверки логики функций, привязанных к событиям карты:

function onMapClick(e) {
    return e.features?.map(f => f.properties.id);
}

Тестирование проводится без реальной карты, через имитацию событийного объекта:

test('onMapClick extracts feature ids', () => {
    const event = {
        features: [
            { properties: { id: 1 } },
            { properties: { id: 2 } }
        ]
    };

    expect(onMapClick(event)).toEqual([1, 2]);
});

Такой подход исключает зависимость от WebGL и рендеринга.


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

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

import maplibregl from 'maplibre-gl';

test('map fires load event', (done) => {
    const map = new maplibregl.Map({
        container: document.createElement('div'),
        style: 'https://example.com/style.json'
    });

    map.on('load', () => {
        done();
    });
});

Ключевая сложность — асинхронная загрузка стиля и тайлов.


Моки WebGL и ограничения среды

В Node.js отсутствует WebGL-контекст, поэтому тестирование требует эмуляции:

  • headless-gl
  • jsdom (частично)
  • мокирование HTMLCanvasElement
  • подмена requestAnimationFrame

Пример мокирования:

global.HTMLCanvasElement.prototype.getContext = () => {
    return {
        createShader: () => {},
        shaderSource: () => {},
        compileShader: () => {},
        createProgram: () => ({}),
        linkProgram: () => {},
        useProgram: () => {}
    };
};

Без этих заглушек инициализация карты завершается ошибкой.


Симуляция событий

MapLibre GL JS позволяет программно вызывать события через методы fire или DOM-эмуляцию.

Симуляция клика

map.on('click', (e) => {
    console.log(e.lngLat);
});

map.fire('click', {
    point: { x: 100, y: 200 },
    lngLat: { lng: 30, lat: 50 }
});

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


Генерация pointer-событий

В E2E тестах часто используется Playwright или Cypress:

await page.mouse.click(200, 150);

После чего проверяется вызов обработчика через UI-эффекты или состояние приложения.


Асинхронность событийной модели

Ключевая особенность тестирования — многослойная асинхронность:

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

События load, render, idle не гарантируют мгновенного исполнения логики.

Типичный антишаблон:

map.on('load', () => {
    expect(map.getSource('data')).toBeDefined();
});

Без ожидания idle источник может быть ещё не готов.


Ожидание стабильного состояния карты

Для тестов используется паттерн ожидания:

function waitForIdle(map) {
    return new Promise((resolve) => {
        const check = () => {
            if (map.isStyleLoaded() && map.loaded()) {
                resolve();
            } else {
                setTimeout(check, 50);
            }
        };
        check();
    });
}

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


Проверка пересечений features

События взаимодействия часто включают features, которые зависят от слоёв:

map.on('click', (e) => {
    console.log(e.features);
});

В тестах важно:

  • убедиться, что слой добавлен (map.addLayer)
  • убедиться, что источник загружен
  • дождаться idle

Без этого features будет пустым массивом.


Тестирование порядка событий

Некоторые сценарии зависят от последовательности:

  • dataloadingdataidle
  • mousemovemouseentermouseleave

Пример проверки порядка:

const events = [];

map.on('dataloading', () => events.push('loading'));
map.on('data', () => events.push('data'));
map.on('idle', () => events.push('idle'));

Тестирование кластеров и источников

При работе с GeoJSON-источниками:

  • события data срабатывают при обновлении данных
  • кластеры могут изменяться после зума
map.on('click', 'clusters', (e) => {
    console.log(e.features[0].properties.cluster_id);
});

Тестирование требует симуляции изменения масштаба:

map.setZoom(10);

Использование spies и mock-обработчиков

В Jest часто применяются spy-функции:

const handler = jest.fn();

map.on('click', handler);

map.fire('click', {
    point: { x: 0, y: 0 },
    lngLat: { lng: 0, lat: 0 }
});

expect(handler).toHaveBeenCalled();

Это позволяет проверять факт вызова без анализа DOM.


Типичные проблемы при тестировании событий

Нестабильность рендера

События могут срабатывать до завершения GPU-процессов.

Дублирование событий

mousemove и render могут вызываться десятки раз в секунду.

Асинхронная загрузка тайлов

load не гарантирует доступность всех данных.

Отсутствие WebGL в тестовой среде

Без моков карта не инициализируется.


Рекомендации по архитектуре тестов событий

  • разделение логики обработчиков и UI-слоя
  • минимизация зависимости от реальной карты в юнит-тестах
  • использование интеграционных тестов только для критических сценариев
  • фиксация тайлов и стилей для детерминированности
  • ожидание idle перед проверками состояния