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

Визуальное регрессионное тестирование для картографических приложений на базе HERE Maps API требует строгой детерминированности рендера, контроля асинхронных процессов загрузки тайлов и стабилизации графического вывода в условиях различных окружений CI/CD. Основная цель — обнаружение непреднамеренных визуальных изменений интерфейса карты, включая смещения тайлов, изменения стилей, искажения подписей, различия в рендеринге слоёв и элементов управления.

Карты представляют собой один из самых нестабильных классов UI с точки зрения пиксельной идентичности. В отличие от DOM-ориентированных интерфейсов, HERE Maps API генерирует визуальный слой через WebGL и canvas, что делает классические snapshot-тесты менее предсказуемыми.

Ключевые источники визуальной нестабильности:

  • асинхронная загрузка тайлов через сеть
  • различия в тайловых серверах и кэшировании
  • вариативность рендера WebGL между GPU
  • плавающие шрифты и антиалиасинг текста
  • динамические подписи объектов (POI, маршруты)
  • округление координат при разных масштабах

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

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

  • Playwright или Puppeteer для управления браузером
  • HERE Maps JavaScript API для рендера карты
  • pixelmatch / image-diff для сравнения изображений
  • CI-среда (GitHub Actions, GitLab CI, Jenkins)
  • система хранения эталонных снимков (baselines)

Основная стратегия заключается в генерации стабильного состояния карты и фиксированного скриншота.

Инициализация HERE Maps API в тестируемом состоянии

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

const platform = new H.service.Platform({
  apikey: process.env.HERE_API_KEY
});

const defaultLayers = platform.createDefaultLayers();

const map = new H.Map(
  document.getElementById('map'),
  defaultLayers.vector.normal.map,
  {
    center: { lat: 52.5200, lng: 13.4050 },
    zoom: 10,
    pixelRatio: 1
  }
);

const beh * avior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));

window.mapInstance = map;

Для тестирования важно отключить динамические эффекты:

map.getViewModel().setLookAtData({
  tilt: 0,
  heading: 0
});

Стабилизация рендера карты

Основная проблема визуальных тестов — недождаться полной отрисовки тайлов. HERE Maps не предоставляет прямого события “render complete”, поэтому применяется комбинация стратегий:

Ожидание завершения загрузки тайлов

function waitForMapIdle(map) {
  return new Promise(resolve => {
    let timer;

    const check = () => {
      const pending = map.getViewPort().getAnimating();
      if (!pending) {
        clearTimeout(timer);
        resolve();
      } else {
        timer = setTimeout(check, 200);
      }
    };

    check();
  });
}

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

map.addEventListener('render', () => {
  window.__rendered = true;
});

Захват скриншота в Playwright

Playwright обеспечивает стабильный headless Chromium с контролем viewport и device scale factor.

import { chromium } from 'playwright';

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage({
    viewport: { width: 1280, height: 720 },
    deviceScaleFactor: 1
  });

  await page.goto('http://localhost:3000/map-test');

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

  await page.waitForTimeout(2000);

  const mapElement = await page.locator('#map');

  await mapElement.screenshot({
    path: 'baseline.png'
  });

  await browser.close();
})();

Pixel-level сравнение изображений

После получения скриншота выполняется сравнение с эталонным изображением.

import fs from 'fs';
import pixelmatch from 'pixelmatch';
import { PNG } from 'pngjs';

const img1 = PNG.sync.read(fs.readFileSync('baseline.png'));
const img2 = PNG.sync.read(fs.readFileSync('current.png'));

const { width, height } = img1;
const diff = new PNG({ width, height });

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

fs.writeFileSync('diff.png', PNG.sync.write(diff));

if (mismatch > 0) {
  throw new Error(`Visual regression detected: ${mismatch} pixels differ`);
}

Порог чувствительности (threshold) подбирается с учётом антиалиасинга и WebGL-шума.

Работа с нестабильностью тайлов

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

1. Отключение анимаций и переходов

map.getViewModel().setAnimationsEnabled(false);

2. Фиксация стиля карты

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

const style = defaultLayers.vector.normal.map;

3. Ограничение области карты

Фиксация bounding box уменьшает количество загружаемых тайлов:

map.getViewModel().setLookAtData({
  bounds: new H.geo.Rect(52.55, 13.35, 52.50, 13.45)
});

Изоляция сетевого окружения

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

В Playwright:

await page.route('**/maptile**', route => {
  route.fulfill({
    status: 200,
    contentType: 'image/png',
    body: fs.readFileSync('./fixtures/tile.png')
  });
});

Это исключает влияние сети и изменений в тайловом CDN.

Учёт различий GPU и рендеринга

WebGL рендер может отличаться между:

  • NVIDIA / AMD / Intel GPU
  • headless vs headed режим
  • Docker без GPU ускорения

Для минимизации расхождений:

  • фиксируется deviceScaleFactor: 1
  • отключается аппаратное ускорение в CI
  • используется software rendering (SwiftShader в Chromium)

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

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

  1. установка зависимостей
  2. запуск локального сервера с картой
  3. генерация baseline (при первом запуске)
  4. выполнение теста рендера
  5. сравнение изображений
  6. публикация diff-артефактов

Пример GitHub Actions шага:

- name: Run visual tests
  run: |
    npm ci
    npm run build
    npm run start &
    npm run test:visual

Стратегии управления baseline-данными

Baseline изображения хранятся:

  • в репозитории (для малых проектов)
  • в S3 / GCS (для масштабных систем)
  • в специализированных сервисах визуального тестирования

При обновлении UI требуется контролируемое обновление baseline через отдельный pipeline.

Борьба с ложными срабатываниями

Основные причины false positives:

  • разница в шрифтах (fallback fonts)
  • субпиксельное позиционирование текста
  • антиалиасинг линий маршрутов
  • различия DPR (devicePixelRatio)
  • порядок загрузки тайлов

Методы снижения:

  • нормализация viewport и DPR
  • фиксированные шрифты через webfont preload
  • увеличение порога pixelmatch
  • маскирование динамических областей (timestamps, UI overlays)

Маскирование динамических элементов

Некоторые элементы карты не должны участвовать в сравнении:

  • popups
  • tooltips
  • live traffic overlays
  • user position markers

Пример маскирования:

await page.evaluate(() => {
  document.querySelectorAll('.h-map-overlay').forEach(el => {
    el.style.display = 'none';
  });
});

Тестирование интерактивных сценариев

Помимо статических скриншотов проверяются состояния:

  • zoom in/out
  • pan по карте
  • смена слоя (satellite, terrain)
  • построение маршрута

Каждое состояние фиксируется отдельным snapshot.

await map.setZoom(14);
await waitForMapIdle(map);

Детектирование деградации производительности

Визуальное тестирование часто дополняется измерением времени рендера:

  • время загрузки тайлов
  • time-to-first-render
  • стабильность FPS
performance.mark('map-start');
// init map
performance.mark('map-end');
performance.measure('map-init', 'map-start', 'map-end');

Масштабирование подхода на крупные системы

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

  • smoke визуальные тесты (основные города/уровни зума)
  • регрессионные наборы (полные сценарии)
  • компонентные тесты UI-оверлеев

Такой подход позволяет балансировать между скоростью CI и полнотой покрытия визуальных состояний.