Инструменты для тестирования

Тестирование приложений на базе MapLibre GL JS строится вокруг ключевой сложности: работа WebGL-рендеринга, асинхронной загрузки тайлов и сложного состояния карты (камера, стиль, источники данных). Это делает классические подходы unit-тестирования недостаточными без дополнительной изоляции графического слоя.


Тестовая стратегия обычно разделяется на четыре уровня:

  • модульные тесты (pure logic)
  • интеграционные тесты (карта + DOM)
  • визуальные регрессионные тесты (рендер)
  • end-to-end тесты (реальный браузер + сеть)

Каждый уровень закрывает свой класс проблем:

  • логика приложения (фильтры, состояния)
  • взаимодействие с картой
  • корректность отображения слоёв
  • поведение в реальном окружении

Модульное тестирование бизнес-логики

На этом уровне MapLibre GL JS вообще не подключается. Проверяется код, который формирует:

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

Пример теста генерации style expression:

export function createHeatmapLayer(intensity) {
  return {
    id: 'heat',
    type: 'heatmap',
    paint: {
      'heatmap-intensity': intensity,
      'heatmap-color': [
        'interpolate',
        ['linear'],
        ['heatmap-density'],
        0, 'blue',
        1, 'red'
      ]
    }
  };
}
import { createHeatmapLayer } from './layers';

test('creates heatmap layer with intensity', () => {
  const layer = createHeatmapLayer(2);
  expect(layer.paint['heatmap-intensity']).toBe(2);
});

Моки MapLibre GL JS и изоляция WebGL

Основная проблема тестирования карт — отсутствие WebGL в Node.js окружении.

Для решения используют:

  • headless WebGL реализации
  • мокирование API карты
  • подмену рендера

Популярные подходы:

1. Mock карты

Создаётся заглушка, имитирующая API:

export class MockMap {
  constructor() {
    this.layers = [];
    this.sources = {};
  }

  addLayer(layer) {
    this.layers.push(layer);
  }

  addSource(id, source) {
    this.sources[id] = source;
  }

  getLayer(id) {
    return this.layers.find(l => l.id === id);
  }
}

Это позволяет тестировать логику добавления слоёв без рендера.


2. headless-gl

Для частичного WebGL окружения используется headless-gl, который эмулирует WebGL context.


3. jest-canvas-mock

В связке с Jest часто применяется мок canvas:

import 'jest-canvas-mock';

Это позволяет избежать ошибок типа:

  • “WebGL not supported”
  • “HTMLCanvasElement.getContext is not a function”

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

Интеграционные тесты запускаются уже в браузерной среде и проверяют:

  • создание карты
  • загрузку стиля
  • добавление источников
  • реакцию на события

Пример с Playwright:

import { test, expect } from '@playwright/test';

test('map renders and loads style', async ({ page }) => {
  await page.goto('http://localhost:3000');

  await page.waitForSelector('.maplibre-map');

  const canvas = await page.locator('canvas');
  await expect(canvas).toBeVisible();
});

Тестирование через Cypress

Cypress часто используют для проверки пользовательских сценариев:

  • перемещение карты
  • клики по объектам
  • всплывающие окна
describe('Map interaction', () => {
  it('zooms in on click', () => {
    cy.visit('/map');

    cy.get('canvas').click(200, 200);

    cy.get('.popup').should('be.visible');
  });
});

Визуальная регрессия

Наиболее важный уровень для MapLibre GL JS — проверка визуального результата.

Подход:

  • фиксируется камера (center, zoom, bearing)
  • отключается анимация
  • делается скриншот canvas
  • сравнение с эталоном

Пример с Playwright:

test('visual regression - map baseline', async ({ page }) => {
  await page.goto('http://localhost:3000/map');

  await page.waitForTimeout(2000);

  const image = await page.screenshot();

  expect(image).toMatchSnapshot('map-baseline.png');
});

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

  • одинаковые тайлы
  • фиксированные данные
  • отключение анимаций

Детеминированность рендера

Карты сложно тестировать из-за недетерминированности:

  • тайлы могут обновляться
  • шрифты подгружаются асинхронно
  • GPU рендер отличается

Для стабилизации применяются:

Фиксация камеры

map.jumpTo({
  center: [0, 0],
  zoom: 5,
  bearing: 0,
  pitch: 0
});

Отключение анимаций

map.setRenderWorldCopies(false);
map.stop();

Локальные тайлы

Используются локальные tile server или фикстуры:

  • geojson источники вместо vector tiles
  • локальные PNG tiles

Мокирование сетевых запросов

При тестировании важно изолировать внешние API.

Используется:

MSW

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

const server = setupServer(
  rest.get('/tiles/:z/:x/:y.pbf', (req, res, ctx) => {
    return res(ctx.body(new Uint8Array([0, 1, 2])));
  })
);

Это позволяет контролировать:

  • ответы тайлов
  • style.json
  • geojson источники

Тестирование style specification

MapLibre использует Style Spec (аналог Mapbox Style Spec). Тестируют:

  • валидность JSON
  • обязательные поля
  • корректность выражений
import style from './style.json';

test('style has sources', () => {
  expect(style.sources).toBeDefined();
});

test('has base layer', () => {
  expect(style.layers.find(l => l.id === 'background')).toBeTruthy();
});

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

Важно проверять события:

  • load
  • click
  • move
  • idle

Пример:

map.on('load', () => {
  map.addLayer({
    id: 'points',
    type: 'circle',
    source: 'geo'
  });
});

Тест проверяет, что layer действительно добавлен после load.


Проверка взаимодействия с источниками данных

Источники могут быть:

  • geojson
  • raster
  • vector tiles

Тестируются сценарии:

  • обновление данных
  • setData вызовы
  • рефреш источников
map.getSource('points').setData({
  type: 'FeatureCollection',
  features: []
});

Производительность и стресс-тесты

Карты чувствительны к:

  • количеству слоёв
  • плотности точек
  • частоте обновлений

Метрики:

  • FPS
  • время загрузки стиля
  • memory usage

Пример теста:

const start = performance.now();

for (let i = 0; i < 100; i++) {
  map.addLayer({ id: `layer-${i}`, type: 'circle', source: 'geo' });
}

const duration = performance.now() - start;
expect(duration).toBeLessThan(200);

CI/CD интеграция

В CI важно учитывать:

  • отсутствие GPU
  • headless браузеры
  • нестабильность тайлов

Рекомендуемая схема:

  • unit: Node + Jest
  • integration: Playwright headless
  • visual: baseline snapshots
  • mock server: MSW

Типичные проблемы тестирования MapLibre GL JS

  • нестабильные скриншоты из-за антиалиасинга WebGL
  • различия рендера между GPU и software context
  • асинхронная загрузка шрифтов
  • фликер тайлов при движении камеры
  • невозможность детерминировать порядок отрисовки векторных слоёв

Эти проблемы решаются через:

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