Snapshot testing

Snapshot testing в контексте Mapbox GL JS применяется как инструмент визуальной регрессии, фиксирующий состояние карты в виде изображения или структурированного представления сцены (DOM + WebGL canvas) и сравнивающий его с эталонным снимком при каждом запуске тестов. В отличие от классических unit-тестов, snapshot-тестирование в картографических приложениях ориентировано не на данные, а на визуальный результат рендеринга сложной сцены, где участвуют тайлы, слои, источники данных, стили и GPU-пайплайн.

Карты в Mapbox GL JS не являются статичным DOM-деревом. Каждый кадр представляет собой результат WebGL-рендеринга, где:

  • источники данных (GeoJSON, vector tiles, raster tiles) загружаются асинхронно;
  • стиль описывается декларативно через style specification;
  • слои могут зависеть от zoom, pitch, bearing и state выражений;
  • итоговый результат формируется GPU и canvas.

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

Базовая стратегия snapshot testing

Основная идея заключается в фиксации состояния карты после полной стабилизации рендера:

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

Пример базового теста с Jest и Playwright:

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

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

  await page.evaluate(() => {
    window.map.setCenter([37.6173, 55.7558]);
    window.map.setZoom(10);
    window.map.setPitch(0);
    window.map.setBearing(0);
  });

  await page.waitForFunction(() => window.map.loaded());

  const mapElement = await page.locator('#map');
  await expect(mapElement).toHaveScreenshot('map-baseline.png');
});

Проблема недетерминированности

Snapshot testing карт сталкивается с фундаментальной проблемой: недетерминированный рендеринг.

Источники нестабильности:

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

Для стабилизации тестов применяются следующие подходы:

Фиксация окружения

  • использование headless Chromium с фиксированной версией;
  • отключение аппаратного ускорения или его унификация;
  • контейнеризация тестового окружения.

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

map.setPaintProperty('water', 'fill-opacity-transition', { duration: 0 });
map.setLayoutProperty('road-label', 'text-rotate', 0);

Также часто полностью отключают transition через стиль:

const style = {
  ...baseStyle,
  transition: { duration: 0 }
};

Фиксация viewport

Любые snapshot-тесты должны выполняться при строго заданных параметрах:

  • width / height контейнера;
  • pixelRatio;
  • zoom level;
  • device scale factor.

Ожидание полной загрузки карты

Критический момент snapshot testing — корректное определение готовности сцены.

Типовые варианты:

await new Promise((resolve) => {
  map.on('idle', resolve);
});

или более строгий вариант:

await map.once('render');
await map.once('idle');

В сложных сценариях необходимо учитывать:

  • загрузку style;
  • загрузку glyphs;
  • загрузку sprites;
  • завершение всех tile requests.

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

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

  • подмена tile server;
  • локальные фикстуры vector tiles;
  • заглушки для sprites и glyphs.

Пример intercept в Playwright:

await page.route('**/tiles/**', route => {
  route.fulfill({
    path: './fixtures/tiles/{z}/{x}/{y}.pbf'
  });
});

Это позволяет полностью убрать зависимость от внешних сервисов Mapbox и других CDN.

Snapshot уровни

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

1. Canvas snapshot

Снимок WebGL canvas:

  • наиболее точный визуально;
  • чувствителен к GPU;
  • сложнее в поддержке.
const canvas = await page.locator('canvas');
await expect(canvas).toHaveScreenshot();

2. DOM snapshot контейнера

Фиксация HTML + overlay UI:

  • полезно для UI слоёв;
  • не отражает полностью карту.

3. Hybrid snapshot

Комбинация canvas + DOM overlays:

  • оптимальный баланс для приложений с UI поверх карты.

Управление стабильностью рендера

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

Использование фиксированных GeoJSON вместо динамических API:

map.addSource('points', {
  type: 'geojson',
  data: '/fixtures/points.json'
});

Отключение label collision randomness

Mapbox GL JS использует сложные алгоритмы размещения подписей. Для стабилизации:

  • фиксируют zoom;
  • избегают динамических данных;
  • используют ограниченные наборы объектов.

Контроль symbol layers

Symbol layers часто являются причиной флейков:

  • порядок размещения текста;
  • приоритеты;
  • collision detection.

Рекомендуется минимизировать плотность данных в snapshot-сценариях.

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

Snapshot testing карт почти всегда выполняется в CI:

  • GitHub Actions;
  • GitLab CI;
  • Jenkins.

Типичный пайплайн:

  1. установка зависимостей;
  2. запуск headless browser;
  3. прогон сценариев;
  4. сравнение с baseline;
  5. генерация diff-отчётов.

Пример команды:

npx playwright test --update-snapshots

Управление эталонными снимками

Эталонные изображения должны:

  • храниться в репозитории;
  • версионироваться;
  • обновляться только осознанно.

При изменении стиля карты (например, изменение цветовой схемы) происходит массовое обновление baseline:

jest --updateSnapshot

или

npx playwright test --update-snapshots

Диффы и анализ изменений

При расхождении snapshot генерируется diff:

  • pixel-level comparison;
  • heatmap различий;
  • bounding boxes изменений.

Типичные причины изменений:

  • изменение style.json;
  • обновление тайлового сервера;
  • изменение шрифтов;
  • обновление версии Mapbox GL JS;
  • изменение браузерного рендеринга.

Частые проблемы и их устранение

Флэйки из-за загрузки ресурсов

Решение:

  • ожидание idle;
  • фикстуры;
  • отключение сети.

Различия между платформами

  • Windows vs Linux различия в font rendering;
  • GPU driver differences.

Решение:

  • Docker с фиксированным образом;
  • Chromium-only environment.

Нестабильность label placement

Решение:

  • уменьшение плотности данных;
  • фиксация zoom и center;
  • использование упрощённых стилей.

Архитектура тестового фреймворка

Типичная структура snapshot testing системы:

  • Test runner (Jest / Playwright)
  • Browser automation layer
  • Map initialization wrapper
  • Fixture manager (tiles, styles)
  • Image diff engine
  • Snapshot storage

Каждый слой изолирует нестабильность WebGL и сети от логики тестов.

Практика масштабирования snapshot testing

При большом количестве карт и стилей:

  • разделение тестов по стилям;
  • параллельный прогон;
  • шардирование snapshot-репозитория;
  • кэширование тайловых данных.

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