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

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

Headless-тестирование в данном контексте означает запуск кода без реального браузерного окна с сохранением возможности проверять:

  • корректность инициализации карты
  • загрузку и применение стилей
  • работу источников (sources)
  • поведение слоёв (layers)
  • реакцию на взаимодействия
  • стабильность рендеринга (snapshot testing)

Ограничения DOM и WebGL в headless-средах

Основная сложность заключается в том, что Mapbox GL JS требует:

  • window и полноценного DOM
  • WebGL контекста (WebGLRenderingContext)
  • тайлового рендера через GPU или его эмуляцию
  • requestAnimationFrame и event loop браузера

В среде Node.js отсутствуют:

  • GPU
  • WebGL API
  • полноценный layout engine браузера

Это приводит к необходимости эмуляции окружения или перехода к альтернативным стратегиям тестирования.

Базовая конфигурация Node.js окружения

Для тестирования обычно используется Node.js с набором polyfill-библиотек:

  • jsdom — эмуляция DOM
  • global polyfills (window, document)
  • fetch mock (или undici mocking)
  • timers (jest fake timers)

Типичная инициализация:

  • создание global.window
  • установка global.document
  • мок requestAnimationFrame
  • заглушка URL.createObjectURL

Однако этого недостаточно для WebGL-части Mapbox GL JS.

Проблема WebGL и подход headless-gl

Mapbox GL JS требует WebGL, который в Node отсутствует. Решение — использование headless реализации:

  • headless-gl (node-canvas/webgl binding)
  • gl package (OSMesa/EGL backend)

Это позволяет создать контекст:

  • gl(width, height, { preserveDrawingBuffer: true })

Далее этот контекст подменяет canvas.getContext('webgl').

Ключевая задача:

  • связать HTMLCanvasElement с headless WebGL
  • обеспечить совместимость API с браузерным поведением

Изоляция Mapbox GL JS в тестах

При тестировании важно контролировать внешние зависимости:

  • запросы к тайлам
  • загрузку стилей JSON
  • спрайты и glyphs
  • токены доступа

Используются подходы:

Перехват сетевых запросов

  • mock fetch API
  • MSW (Mock Service Worker)
  • кастомный interceptor

Пример логики:

  • все mapbox://styles/... заменяются на локальные JSON
  • tile requests возвращают фикстуры

Локальные стили

Стиль карты фиксируется:

  • упрощённый JSON style
  • минимальный набор layers
  • отсутствие внешних URL

Это снижает нестабильность тестов.

Unit-тестирование логики карты

В unit-тестах проверяется не рендер, а поведение API:

  • map.addSource
  • map.addLayer
  • map.setFilter
  • map.setLayoutProperty

Используется стратегия:

  • создание карты в headless окружении
  • проверка состояния через map.getStyle()

Пример проверяемых аспектов:

  • слой добавлен
  • порядок слоёв корректен
  • источник зарегистрирован
  • фильтр применён

Snapshot-тестирование стилей

Один из наиболее эффективных подходов — snapshot testing:

  • фиксируется итоговый style JSON
  • сравнивается после изменений

Проверяются:

  • структура layers
  • layout properties
  • paint properties
  • transitions

Преимущество:

  • отсутствие необходимости WebGL

Недостаток:

  • не проверяет визуальный результат напрямую

Рендер-снапшоты (pixel testing)

Более сложный подход — визуальные снапшоты:

  • рендер карты в offscreen canvas
  • экспорт изображения (canvas.toDataURL)
  • сравнение пикселей

Требуется:

  • headless WebGL
  • стабильная среда выполнения
  • фиксированные данные тайлов

Используются библиотеки:

  • pixelmatch
  • jest-image-snapshot

Проблемы:

  • флаки тесты при изменении антиалиасинга
  • различия GPU/OS
  • нестабильность шрифтов

Использование Puppeteer и Playwright

Для более реалистичного тестирования применяется браузерный headless режим:

Playwright Puppeteer

Преимущества:

  • реальный Chromium WebGL
  • точное поведение рендера
  • поддержка сетевых interception API

Типовой сценарий:

  • запуск headless Chromium
  • загрузка страницы с Mapbox GL JS
  • ожидание map.load
  • снимок canvas или DOM

Дополнительные возможности:

  • эмуляция разных viewport
  • тестирование взаимодействий (drag, zoom)
  • проверка performance metrics

Мокирование Mapbox GL JS

В некоторых сценариях библиотека полностью мокается:

  • замена Map на stub-класс

  • эмуляция событий:

    • load
    • render
    • idle

Проверяются только вызовы API:

  • new Map()
  • map.on('load')
  • map.addLayer(...)

Такой подход применяется:

  • в unit-тестах бизнес-логики
  • в React/Vue компонентах

Тестирование React-обёрток

При использовании Mapbox GL JS внутри UI-фреймворков:

  • проверяется lifecycle карты
  • уничтожение инстанса (map.remove)
  • повторная инициализация

Критично тестировать:

  • отсутствие утечек памяти
  • корректный cleanup event listeners

Асинхронность и ожидание состояния карты

Mapbox GL JS работает через события:

  • load
  • idle
  • render

В тестах важно синхронизировать выполнение:

  • await new Promise(resolve => map.on('load', resolve))
  • ожидание idle перед snapshot

Без этого тесты становятся нестабильными.

Работа с тайлами и офлайн-режимом

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

  • локальный tile server
  • фикстуры vector tiles
  • отключение сетевых запросов

Подход:

  • заранее подготовленные .pbf данные
  • локальные style sources

Это позволяет:

  • воспроизводить одинаковый рендер
  • избегать зависимости от CDN

CI-окружения и стабильность

В CI (GitHub Actions, GitLab CI):

  • нет GPU
  • ограничен WebGL

Поэтому применяются стратегии:

  • headless-gl fallback
  • Playwright Chromium
  • отключение антиалиасинга
  • фиксированные шрифты

Важно:

  • закрепление версий Chromium
  • фиксация Mapbox GL JS версии
  • контроль таймаутов загрузки

Частые проблемы и причины нестабильности тестов

  • различие WebGL реализаций
  • асинхронная загрузка ресурсов
  • race conditions при map.load
  • различия в рендере текста
  • случайный порядок tile requests
  • кэширование браузера

Для устранения:

  • отключение кэша
  • deterministic fixtures
  • явные ожидания событий
  • контроль request order

Стратегии комбинированного тестирования

На практике применяется гибрид:

  • unit тесты (API и состояние)
  • snapshot тесты (style JSON)
  • integration headless-gl (рендер)
  • e2e Playwright (реальный браузер)

Такой подход покрывает:

  • логику
  • конфигурацию
  • визуальный результат
  • пользовательские сценарии