Визуальное тестирование

Визуальное тестирование в приложениях на основе OpenLayers отличается от классического UI-тестирования высокой чувствительностью к рендерингу. Карта представляет собой композицию множества слоёв, каждый из которых может влиять на итоговое изображение: тайлы, векторные объекты, стили, подписи, анимации и WebGL-рендеринг.

Ключевая проблема заключается в том, что результат работы карты — это не DOM-структура, а пиксельное изображение, формируемое в <canvas> или WebGL-контексте. Любое изменение в зависимости, браузере, шрифте или даже порядке загрузки тайлов может привести к различиям на уровне пикселей.

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

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

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

Рендеринг в OpenLayers строится вокруг нескольких ключевых компонентов:

  • Map renderer — управляет циклом отрисовки
  • Layer renderer — отвечает за конкретный слой
  • Source — источник данных (XYZ, WMS, vector, GeoJSON)
  • View — состояние камеры (центр, zoom, rotation)

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

Особенно критичны:

  • пересчёт проекции (EPSG:3857, EPSG:4326)
  • интерполяция координат при зуме
  • кэширование тайлов
  • дебаунс перерисовки

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

map.getView().setCenter([0, 0]);
map.getView().setZoom(5);
map.getView().setRotation(0);

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


Подходы к визуальному тестированию карт

Скриншотное сравнение (snapshot testing)

Базовый подход — сравнение эталонного изображения с текущим рендером карты.

Типовой цикл:

  1. Инициализация карты в headless-браузере
  2. Ожидание завершения загрузки всех тайлов
  3. Снятие скриншота canvas или контейнера
  4. Сравнение с эталоном по пикселям

Пример с использованием Playwright:

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

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

  await page.waitForFunction(() => {
    return window.mapLoaded === true;
  });

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

Ключевая проблема — необходимость стабилизации тайлов и шрифтов.


Пиксельное сравнение

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

Используется алгоритмическое сравнение:

  • абсолютная разница RGB
  • процент отличающихся пикселей
  • структурное сравнение (SSIM)

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

const diff = pixelmatch(img1.data, img2.data, null, width, height, {
  threshold: 0.1
});

Такой подход позволяет учитывать допустимые отклонения в антиалиасинге.


Фиксация состояния карты

Для стабильного тестирования необходимо полностью детерминировать состояние:

  • отключение анимаций
  • фиксация времени (для time-based слоёв)
  • запрет плавных переходов
map.getView().animate = () => {};
map.renderSync();

Также важно отключать инерцию:

import { defaults as defaultInteractions } from 'ol/interaction';

const map = new Map({
  interactions: defaultInteractions({
    pinchRotate: false,
    doubleClickZoom: false
  })
});

Проблема тайловых слоёв

Tile-based источники (XYZ, OSM, WMTS) являются основной причиной нестабильности визуальных тестов.

Причины:

  • сетевые задержки
  • кэш браузера
  • различие CDN
  • обновление тайлов

Подмена тайлового источника

Используется локальный фиктивный сервер или мокирование:

const source = new XYZ({
  url: 'http://localhost:8081/tiles/{z}/{x}/{y}.png'
});

или полностью статический слой:

const source = new Static({
  url: '/fixtures/tile.png',
  imageExtent: [0, 0, 256, 256]
});

Векторные слои и детерминизм

Векторные данные более стабильны, но подвержены другим проблемам:

  • различия в порядке отрисовки
  • сортировка feature-ов
  • z-index и geometry overlap

Принудительная стабилизация порядка:

vectorSource.getFeatures().sort((a, b) => {
  return a.getId() - b.getId();
});

Также важно фиксировать стили:

const style = new Style({
  fill: new Fill({ color: 'rgba(0, 150, 255, 0.5)' }),
  stroke: new Stroke({ color: '#3399CC', width: 2 })
});

WebGL-рендеринг и его влияние на тесты

OpenLayers поддерживает WebGL для ускоренной отрисовки точечных и векторных данных.

Проблема заключается в:

  • различии GPU
  • драйверах
  • precision float расчетов
  • браузерных оптимизациях

Для тестирования часто используется отключение WebGL:

import WebGLPointsLayer from 'ol/layer/WebGLPoints';

const layer = new WebGLPointsLayer({
  disable: true
});

или принудительный Canvas renderer:

const map = new Map({
  renderer: 'canvas'
});

Headless-браузеры и окружение тестирования

Наиболее стабильная среда — Chromium в headless-режиме.

Критические параметры окружения:

  • фиксированный viewport
  • отключение GPU ускорения
  • единые шрифты
  • отключение аппаратного сглаживания
await page.setViewportSize({ width: 1280, height: 720 });

Для Docker-окружений часто добавляется:

  • установка пакетов fonts-dejavu
  • фиксация версии Chromium
  • отключение sandbox только при необходимости

Стабилизация шрифтов и текста

Подписи на карте часто ломают визуальные тесты.

Причины:

  • системные различия font-rendering
  • fallback-шрифты
  • subpixel rendering

Решения:

  • использование web-safe шрифтов
  • фиксация через @font-face
  • ожидание загрузки шрифтов
await page.evaluate(() => document.fonts.ready);

Инструменты сравнения изображений

Pixelmatch

Используется для базового pixel diff:

  • быстрый
  • простой
  • подходит для CI

Applitools

Семантическое визуальное сравнение:

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

Percy

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

  • хранение baseline
  • диффы в облаке
  • история изменений

CI-пайплайн визуального тестирования

Типичный pipeline включает:

  1. запуск локального tile-mock сервера
  2. старт headless Chromium
  3. выполнение сценариев OpenLayers
  4. генерация скриншотов
  5. сравнение с baseline

Пример структуры:

npm run start:test-server
npm run test:visual
npm run diff:images

В CI важно обеспечить:

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

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

OpenLayers рендерит сцены в несколько этапов, поэтому простого await page.waitForTimeout недостаточно.

Используются сигналы готовности:

map.once('rendercomplete', () => {
  window.mapReady = true;
});

или polling:

await page.waitForFunction(() => window.mapReady === true);

Контроль детерминированности проекций

Проекционные преобразования могут давать микросдвиги.

Особенно критично:

  • Web Mercator (EPSG:3857)
  • трансформации при high zoom levels
  • округление координат

Фиксация состояния:

view.setConstrainResolution(true);
view.setCenter([0, 0]);
view.setResolution(156543.0339);

Работа с анимациями

Любые transition-эффекты нарушают визуальное сравнение.

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

view.setAnimate(false);

или глобально:

map.beforeRender = () => true;

Стратегии минимизации ложных срабатываний

Основные подходы:

  • tolerance-based diff
  • masking динамических областей
  • сегментация карты на зоны
  • сравнение только интересующих слоёв

Маскирование:

const mask = {
  x: 0,
  y: 0,
  width: 200,
  height: 100
};

Стабильные baseline-стратегии

Baseline-изображения должны формироваться:

  • на одной машине
  • в фиксированной среде
  • с фиксированным набором тайлов
  • при отключённых внешних зависимостях

Часто используется:

  • Docker image с браузером
  • pinned npm dependencies
  • локальный tile server

Ошибки при визуальном тестировании картографических приложений

Наиболее частые проблемы:

  • тестирование без ожидания rendercomplete
  • использование реальных тайловых серверов
  • отсутствие фиксации zoom/center
  • различия в CI и локальной среде
  • игнорирование WebGL-отличий

Разделение тестовых уровней

Визуальное тестирование в OpenLayers обычно разделяется на:

  • component-level: отдельный слой
  • map-level: вся карта
  • interaction-level: зум, панорамирование
  • regression-level: сравнение baseline

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