Мокирование MapLibre

Причины необходимости мокирования в контексте MapLibre GL JS

Библиотека MapLibre GL JS работает поверх WebGL и активно взаимодействует с браузерным окружением: DOM, графическим контекстом, сетевыми запросами к тайлам и стилям, системой событий и асинхронной загрузкой ресурсов. Это создаёт сложность при тестировании и выполнении кода вне реального браузера.

Основные источники нестабильности:

  • зависимость от WebGL-контекста (canvas.getContext('webgl'))
  • загрузка тайлов по сети (XYZ, vector tiles, raster tiles)
  • асинхронная загрузка style.json и sprite-ресурсов
  • обработка событий (load, render, idle)
  • использование requestAnimationFrame
  • таймеры и внутренний цикл рендера

Мокирование устраняет эти зависимости, заменяя их предсказуемыми заглушками.


Архитектурные точки, подлежащие мокированию

Инициализация карты

Конструктор Map создаёт сложную цепочку внутренних процессов:

  • создание WebGL-контекста
  • загрузка стиля
  • инициализация источников (sources)
  • постановка задач рендера

В тестовой среде эти шаги заменяются упрощённой моделью:

class MockMap {
  constructor(options) {
    this.options = options;
    this.style = options.style || {};
    this._listeners = {};
    this.loaded = false;
  }

  on(event, handler) {
    this._listeners[event] = this._listeners[event] || [];
    this._listeners[event].push(handler);
  }

  _emit(event, payload) {
    (this._listeners[event] || []).forEach(fn => fn(payload));
  }

  load() {
    this.loaded = true;
    this._emit('load');
  }
}

Мокирование WebGL контекста

WebGL в jsdom отсутствует, поэтому критическая точка — getContext.

Подходы:

1. Полный stub canvas

HTMLCanvasElement.prototype.getContext = () => ({
  canvas: {},
  getExtension: () => null,
  createShader: () => ({}),
  shaderSource: () => {},
  compileShader: () => {},
  createProgram: () => ({}),
  linkProgram: () => {},
  useProgram: () => {},
});

Такой подход подходит для поверхностных тестов, где рендер не проверяется.


2. headless-gl (псевдо-WebGL)

Использование gl:

import createGL from 'gl';

const gl = createGL(256, 256, { preserveDrawingBuffer: true });

Преимущество — частичная совместимость с реальными вызовами WebGL.

Недостаток — сложность настройки и нестабильность в CI.


3. Полное отключение рендера

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

const map = new MockMap({
  renderWorldCopies: false,
  interactive: false
});

map._render = () => {};

Используется в unit-тестах бизнес-логики поверх карты.


Мокирование сетевого слоя

MapLibre загружает:

  • style.json
  • vector tiles (.pbf)
  • raster tiles (.png, .jpg)
  • sprites (.json, .png)

Перехват fetch

global.fetch = jest.fn((url) => {
  if (url.includes('style.json')) {
    return Promise.resolve({
      json: () => Promise.resolve({
        version: 8,
        sources: {},
        layers: []
      })
    });
  }

  return Promise.resolve({
    arrayBuffer: () => Promise.resolve(new ArrayBuffer(8))
  });
});

Использование MSW (Mock Service Worker)

Более структурированный подход:

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/style.json', (req, res, ctx) => {
    return res(ctx.json({
      version: 8,
      sources: {},
      layers: []
    }));
  }),

  rest.get('/tiles/:z/:x/:y.pbf', (req, res, ctx) => {
    return res(ctx.body(new ArrayBuffer(10)));
  })
);

MSW обеспечивает реалистичное поведение HTTP без изменения кода приложения.


Мокирование событийной модели

MapLibre активно использует события:

  • load
  • render
  • idle
  • error
  • data

Для тестирования важно контролировать их вручную.

const map = new MockMap({});

let loaded = false;

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

map.load();

expect(loaded).toBe(true);

Мокирование методов MapLibre API

Часто требуется проверка вызовов API:

  • setCenter
  • setZoom
  • flyTo
  • setStyle
  • addSource
  • addLayer
const map = {
  setCenter: jest.fn(),
  setZoom: jest.fn(),
  flyTo: jest.fn(),
  addSource: jest.fn(),
  addLayer: jest.fn()
};

Применение:

map.setCenter([30, 50]);

expect(map.setCenter).toHaveBeenCalledWith([30, 50]);

Мокирование style specification

Стиль в MapLibre — ключевой источник сложности. Его структура включает:

  • layers
  • sources
  • glyphs
  • sprite

Упрощённая модель для тестов:

const mockStyle = {
  version: 8,
  sources: {
    cities: {
      type: 'geojson',
      data: { type: 'FeatureCollection', features: [] }
    }
  },
  layers: [
    {
      id: 'cities-layer',
      type: 'circle',
      source: 'cities'
    }
  ]
};

Мокирование тайлового слоя

Tile-система является асинхронной и потоковой.

Упрощённая заглушка:

class MockTile {
  constructor(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
    this.loaded = false;
  }

  load() {
    this.loaded = true;
  }
}

Используется для тестирования логики кеширования и запросов.


Изоляция requestAnimationFrame

MapLibre рендерит карту через RAF. В тестах он заменяется:

global.requestAnimationFrame = (cb) => {
  return setTimeout(cb, 0);
};

global.cancelAnimationFrame = (id) => {
  clearTimeout(id);
};

Это делает рендер детерминированным.


Подмена MapLibre через factory-обёртку

Для архитектурной изоляции применяется инъекция зависимостей:

export function createMap(MapImpl, options) {
  return new MapImpl(options);
}

В тестах:

const map = createMap(MockMap, {});

Библиотеки мокирования MapLibre

Существуют специализированные решения:

  • mapbox-gl-js-mock (совместим с MapLibre API)
  • самописные MockMap классы
  • Jest manual mocks (__mocks__ директория)

Пример Jest mock:

__mocks__/maplibre-gl.js
export const Map = jest.fn().mockImplementation(() => ({
  on: jest.fn(),
  addSource: jest.fn(),
  addLayer: jest.fn(),
  setStyle: jest.fn()
}));

Мокирование для интеграционных тестов UI

При тестировании React/Vue компонентов с картой используется частичная эмуляция:

  • карта не рендерится
  • DOM контейнер существует
  • API вызовы фиксируются
jest.mock('maplibre-gl', () => ({
  Map: jest.fn(() => ({
    on: jest.fn(),
    remove: jest.fn(),
    addControl: jest.fn()
  }))
}));

Контроль состояния загрузки карты

Ключевые состояния:

  • initializing
  • loading
  • loaded
  • error

Мок-модель:

const state = {
  initialized: true,
  styleLoaded: true,
  sourcesLoaded: true,
  rendered: true
};

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


Мокирование ошибок рендера

Тестирование устойчивости требует имитации ошибок WebGL:

const errorMap = {
  on: (event, cb) => {
    if (event === 'error') {
      cb(new Error('WebGL context lost'));
    }
  }
};

Ограничения мокирования

Полная имитация MapLibre невозможна без значительного упрощения:

  • нет реального рендеринга слоёв
  • отсутствует производительность GPU pipeline
  • не воспроизводится поведение кластеризации и z-order
  • упрощается тайловая синхронизация

Поэтому мокирование используется только для:

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