Визуальное регрессионное тестирование для картографических приложений на базе HERE Maps API требует строгой детерминированности рендера, контроля асинхронных процессов загрузки тайлов и стабилизации графического вывода в условиях различных окружений CI/CD. Основная цель — обнаружение непреднамеренных визуальных изменений интерфейса карты, включая смещения тайлов, изменения стилей, искажения подписей, различия в рендеринге слоёв и элементов управления.
Карты представляют собой один из самых нестабильных классов UI с точки зрения пиксельной идентичности. В отличие от DOM-ориентированных интерфейсов, HERE Maps API генерирует визуальный слой через WebGL и canvas, что делает классические snapshot-тесты менее предсказуемыми.
Ключевые источники визуальной нестабильности:
Для стабильного визуального тестирования используется связка headless-браузера и инструмента сравнения изображений:
Основная стратегия заключается в генерации стабильного состояния карты и фиксированного скриншота.
Ключевой принцип — полная детерминированность карты: фиксированный центр, масштаб, стиль и отключение анимаций.
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 обеспечивает стабильный 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();
})();
После получения скриншота выполняется сравнение с эталонным изображением.
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 загружают тайлы динамически, что создаёт флаки-тесты. Для стабилизации применяются следующие подходы:
map.getViewModel().setAnimationsEnabled(false);
Используется строго один стиль без динамических переключений:
const style = defaultLayers.vector.normal.map;
Фиксация 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.
WebGL рендер может отличаться между:
Для минимизации расхождений:
deviceScaleFactor: 1Типовой pipeline включает:
Пример GitHub Actions шага:
- name: Run visual tests
run: |
npm ci
npm run build
npm run start &
npm run test:visual
Baseline изображения хранятся:
При обновлении UI требуется контролируемое обновление baseline через отдельный pipeline.
Основные причины false positives:
Методы снижения:
Некоторые элементы карты не должны участвовать в сравнении:
Пример маскирования:
await page.evaluate(() => {
document.querySelectorAll('.h-map-overlay').forEach(el => {
el.style.display = 'none';
});
});
Помимо статических скриншотов проверяются состояния:
Каждое состояние фиксируется отдельным snapshot.
await map.setZoom(14);
await waitForMapIdle(map);
Визуальное тестирование часто дополняется измерением времени рендера:
performance.mark('map-start');
// init map
performance.mark('map-end');
performance.measure('map-init', 'map-start', 'map-end');
В крупных приложениях применяется разделение тестов:
Такой подход позволяет балансировать между скоростью CI и полнотой покрытия визуальных состояний.