Snapshot тестирование

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

Картографический рендеринг относится к категории графически сложных и частично недетерминированных процессов. Даже при идентичном входном состоянии карта может давать различия на уровне пикселей из-за особенностей:

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

Snapshot-тестирование в таких условиях требует не только фиксации результата, но и строгой стабилизации окружения.

Базовая модель snapshot-пайплайна

Типичный цикл snapshot-тестирования карты включает следующие этапы:

  1. Инициализация карты с фиксированным стилем
  2. Установка камеры (центр, zoom, bearing, pitch)
  3. Ожидание полной загрузки источников данных
  4. Ожидание завершения рендеринга
  5. Захват изображения (canvas или screenshot)
  6. Сравнение с эталонным изображением

Ключевой момент — момент фиксации состояния. В экосистеме WebGL-карт наиболее стабильным сигналом является событие idle, означающее завершение всех рендеринговых задач.

map.on('load', () => {
  map.once('idle', () => {
    const image = map.getCanvas().toDataURL();
  });
});

Детеминизация рендера MapLibre GL JS

Для получения стабильных snapshot-результатов требуется отключение или контроль следующих факторов:

Анимации

Любые плавные переходы камеры или стилей приводят к промежуточным состояниям.

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

Использование jumpTo вместо easeTo или flyTo устраняет временную неопределённость.

Тайлы и сетевые зависимости

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

sources: {
  cities: {
    type: 'geojson',
    data: '/fixtures/cities.geojson'
  }
}

Шрифты и glyphs

Отображение текста является одним из главных источников нестабильности. Используются:

  • локальные glyphs;
  • предзагруженные sprite-атласы;
  • фиксированные font-family.

Сприты и стили

Любое изменение sprite JSON или PNG приводит к каскадным визуальным изменениям. Поэтому snapshot-тестирование требует фиксации версии стиля.

Методы захвата изображения

Canvas snapshot

Наиболее прямой способ — извлечение изображения из WebGL canvas:

const canvas = map.getCanvas();
const dataURL = canvas.toDataURL('image/png');

Однако этот метод зависит от параметра preserveDrawingBuffer, который должен быть включён:

const map = new maplibregl.Map({
  container: 'map',
  style: style,
  preserveDrawingBuffer: true
});

Без этого параметра WebGL может очищать буфер после кадра, делая снимок невозможным.

Browser screenshot

Более надёжный способ — использование headless-браузера (Playwright или Puppeteer):

await page.screenshot({ path: 'snapshot.png' });

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

Интеграция с тестовыми фреймворками

Snapshot-тестирование карт обычно интегрируется с Jest или аналогичными системами.

Jest + pixel diff

Базовая схема:

  1. Генерация изображения
  2. Сравнение с эталоном
  3. Вычисление diff
import { toMatchImageSnapshot } from 'jest-image-snapshot';

expect.extend({ toMatchImageSnapshot });

test('map snapshot', async () => {
  const image = await captureMap();
  expect(image).toMatchImageSnapshot();
});

Под капотом часто используется pixelmatch, который сравнивает изображения на уровне пикселей с заданным порогом различий.

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

Стабильность snapshot зависит от полного контроля состояния рендера.

Ожидание загрузки слоёв

MapLibre GL JS предоставляет события, однако их недостаточно для полной гарантии.

map.on('idle', () => {
  // карта стабилизирована
});

Для сложных сцен применяется дополнительная проверка:

  • отсутствие tilesloading
  • отсутствие rendering
  • завершение загрузки glyphs

Фиксация viewport

Snapshot всегда зависит от камеры:

map.setCenter([37.6173, 55.7558]);
map.setZoom(10);
map.setBearing(0);
map.setPitch(0);

Любая вариативность камеры делает тест нестабильным.

Проблемы пиксельной стабильности

Антиалиасинг

WebGL использует сглаживание, которое может различаться на разных GPU. Даже минимальные отличия приводят к ложным срабатываниям.

Разные окружения CI

Headless Chrome в CI и локальный Chrome могут давать разные результаты рендеринга.

DPI scaling

DevicePixelRatio влияет на итоговое изображение:

devicePixelRatio: 1

Для тестов фиксируется значение 1.

Стратегии снижения флейковости

Пороговое сравнение

Используется tolerance-based сравнение:

  • допустимое количество пикселей с отклонением
  • процентное отклонение изображения
{
  failureThreshold: 0.01,
  failureThresholdType: 'percent'
}

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

Динамические элементы (например, подписи дат или метки времени) исключаются из сравнения.

Фиксированные тайлы

Использование локального tile-server устраняет сетевую неопределённость.

Headless WebGL и окружение

Snapshot тестирование карт часто требует включения GPU-ускорения даже в headless режиме:

chromium --headless --use-gl=swiftshader

SwiftShader обеспечивает программный рендеринг, уменьшая зависимость от железа.

Тестирование стилей MapLibre

Snapshot применяется не только к изображению карты, но и к стилям:

  • layout слоёв;
  • visibility;
  • фильтры;
  • выражения (expressions).

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

Контроль источников данных

GeoJSON и vector tiles должны быть детерминированными:

  • фиксированный порядок объектов;
  • отсутствие серверной сортировки;
  • стабильные ID сущностей.

Даже порядок полигонов может повлиять на визуальный результат при наложении слоёв.

Масштабируемые сценарии snapshot-тестирования

Многоуровневые сцены

Снимки делаются на разных zoom-уровнях:

  • z=0 (глобальная карта)
  • z=5 (региональный уровень)
  • z=10+ (городской уровень)

Сценарии взаимодействия

Snapshot может фиксировать состояния:

  • hover
  • selected feature
  • highlighted layer

Архитектура тестового стенда

Типовая архитектура включает:

  • headless browser (Playwright)
  • тестовый раннер (Jest)
  • image diff engine (pixelmatch)
  • локальный tile server
  • фиксированные assets (sprites, glyphs, fonts)

Проблемы масштабирования snapshot-наборов

При увеличении числа тестов возникают:

  • рост объёма эталонных изображений;
  • необходимость обновления baseline при изменениях стиля;
  • увеличение времени CI.

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

  • группировка snapshot по стилям;
  • инкрементальные обновления;
  • разделение UI и map snapshots.

Роль snapshot-тестирования в картографических интерфейсах

Визуальные регрессии в картах критичны, поскольку:

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

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