Тестирование взаимодействий

Взаимодействия в Deck.gl строятся вокруг системы picking — механизма определения объектов под курсором и генерации событий уровня слоя. В отличие от DOM-событий, здесь ключевую роль играет рендер в WebGL-контексте и вычисление попаданий через offscreen-pass. Это формирует специфический класс тестов, где проверяется не только логика обработчиков, но и корректность работы слоя в условиях графического рендера.

Основные точки взаимодействия:

  • onClick — обработка клика по объектам слоя
  • onHover — реакции на наведение
  • onDragStart / onDrag / onDragEnd — события перетаскивания
  • pickObject и pickMultipleObjects — программный вызов механизма picking
  • пользовательские события через EventManager

Тестирование этих механизмов требует разделения логики на уровни: слой, контейнер (Deck / DeckGL) и интеграция с картографическим движком (например, Mapbox GL).


Особенности тестирования WebGL picking

Picking в Deck.gl реализуется через дополнительный рендеринг в буфер, где каждый объект кодируется уникальным идентификатором. При тестировании возникает несколько проблем:

  • невозможность напрямую проверить GPU-буфер в стандартной среде Jest
  • асинхронность рендеринга WebGL
  • зависимость от размеров viewport и состояния камеры
  • различия между headless и headful режимами

Поэтому тесты делятся на:

  • юнит-тесты логики слоёв
  • интеграционные тесты rendering + picking
  • e2e тесты взаимодействий в браузере

Юнит-тестирование событий слоя

На уровне слоя проверяется только корректность формирования pickingInfo и реакций на входные события.

Пример теста логики onHover:

import {ScatterplotLayer} from '@deck.gl/layers';

test('onHover вызывается с корректным pickingInfo', () => {
  const mock = jest.fn();

  const layer = new ScatterplotLayer({
    id: 'points',
    data: [{position: [0, 0]}],
    getPosition: d => d.position,
    onHover: mock
  });

  const pickingInfo = {
    object: {position: [0, 0]},
    x: 100,
    y: 200,
    coordinate: [0, 0],
    layer
  };

  layer.props.onHover(pickingInfo);

  expect(mock).toHaveBeenCalledWith(pickingInfo);
});

Здесь проверяется не WebGL, а контракт слоя: корректность передачи данных и отсутствие побочных эффектов.


Использование @deck.gl/test-utils

Библиотека @deck.gl/test-utils предоставляет инструменты для создания mock-дека и симуляции рендера.

Ключевые возможности:

  • создание тестового экземпляра Deck
  • имитация viewport
  • вызов pickObject
  • рендер без реального canvas

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

import {createDeckInstance} from '@deck.gl/test-utils';
import {ScatterplotLayer} from '@deck.gl/layers';

test('pickObject возвращает объект слоя', () => {
  const deck = createDeckInstance({
    layers: [
      new ScatterplotLayer({
        id: 'points',
        data: [{position: [10, 10]}],
        getPosition: d => d.position
      })
    ]
  });

  const info = deck.pickObject({x: 0, y: 0});

  expect(info).toBeDefined();
  expect(info.object).toBeDefined();
});

Этот подход позволяет тестировать логику picking без полноценного браузера.


Интеграционные тесты с WebGL контекстом

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

  • слой
  • WebGL контекст
  • viewport
  • события мыши

Чаще всего используется headless WebGL через headless-gl или puppeteer.

Пример через Puppeteer:

await page.mouse.move(100, 100);
await page.mouse.click(100, 100);

const result = await page.evaluate(() => window.__LAST_PICK_INFO__);

expect(result.object).toBeTruthy();

В приложении при этом сохраняется последний pickingInfo:

const deck = new Deck({
  layers: [...],
  onClick: info => {
    window.__LAST_PICK_INFO__ = info;
  }
});

Тестирование hover-состояний и частых обновлений

onHover может вызываться при каждом движении мыши, что создаёт поток событий. В тестах важно избегать избыточных ассертов и контролировать частоту вызовов.

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

  • debounce в тестовой среде
  • фиксация координат мыши
  • проверка только последнего состояния

Пример:

jest.useFakeTimers();

const hoverMock = jest.fn();

layer.props.onHover({x: 10, y: 10});
layer.props.onHover({x: 11, y: 11});
layer.props.onHover({x: 12, y: 12});

jest.runAllTimers();

expect(hoverMock).toHaveBeenCalledTimes(3);

Симуляция drag-интеракций

Drag-логика в Deck.gl опирается на последовательность событий:

  1. pointerdown
  2. pointermove
  3. pointerup

В тестах важно сохранять состояние взаимодействия между вызовами.

Пример:

const layer = new ScatterplotLayer({
  id: 'drag',
  data: [{position: [0, 0]}],
  pickable: true,
  onDragStart: jest.fn(),
  onDrag: jest.fn(),
  onDragEnd: jest.fn()
});

layer.props.onDragStart({x: 0, y: 0, object: {position: [0, 0]}});

layer.props.onDrag({x: 10, y: 10, delta: [10, 10]});

layer.props.onDragEnd({x: 10, y: 10});

Тестируется:

  • корректность последовательности вызовов
  • сохранение объекта взаимодействия
  • отсутствие пропусков событий

Проверка взаимодействий в React-обёртке DeckGL

При использовании @deck.gl/react добавляется слой React-событий, который требует отдельного тестирования через React Testing Library или Enzyme.

Пример:

import {render, fireEvent} from '@testing-library/react';

test('click вызывает onClick слой', () => {
  const onCl ick = jest.fn();

  render(
    <DeckGL
      initialViewState={{longitude: 0, latitude: 0, zoom: 1}}
      controller={true}
      layers={[
        new ScatterplotLayer({
          id: 'points',
          data: [{position: [0, 0]}],
          onClick
        })
      ]}
    />
  );

  fireEvent.click(document.querySelector('canvas'));

  expect(onClick).toHaveBeenCalled();
});

Основная сложность заключается в том, что React не управляет WebGL напрямую, поэтому тест проверяет только связку событий, а не сам rendering pipeline.


Изоляция viewport и камеры

Многие ошибки взаимодействий связаны с неверным состоянием камеры:

  • неправильный zoom влияет на hit detection
  • смещение viewport меняет координаты picking
  • rotation может ломать ожидания координат

В тестах фиксируется viewport:

const viewState = {
  longitude: 0,
  latitude: 0,
  zoom: 10,
  pitch: 0,
  bearing: 0
};

И проверяется стабильность результата picking:

const info1 = deck.pickObject({x: 50, y: 50});
const info2 = deck.pickObject({x: 50, y: 50});

expect(info1.object).toEqual(info2.object);

Проверка кастомных eventManager-обработчиков

Deck.gl позволяет расширять систему событий через EventManager. Это требует отдельного тестирования пользовательских событий.

Пример:

const eventManager = new EventManager(document.body, {
  events: ['click', 'pointermove']
});

eventManager.on({
  click: e => {
    result = e;
  }
});

document.body.dispatchEvent(new MouseEvent('click'));

expect(result.type).toBe('click');

Стратегия покрытия взаимодействий

Полное тестирование взаимодействий в Deck.gl обычно строится по слоям:

  • логика слоя → unit
  • picking → mock / test-utils
  • WebGL pipeline → headless integration
  • UI → e2e через Puppeteer/Playwright

Критически важные проверки:

  • корректность pickingInfo
  • стабильность координат
  • последовательность drag-событий
  • отсутствие утечек состояния между взаимодействиями
  • идентичность результатов pick при повторных вызовах

Частые ошибки в тестировании взаимодействий

  • попытка тестировать WebGL-рендер как DOM
  • отсутствие фиксации viewport
  • игнорирование асинхронности render loop
  • проверка hover без учёта частоты событий
  • использование реального canvas без стабилизации окружения

Такие ошибки приводят к нестабильным тестам, которые ломаются при изменении таймингов или GPU-поведения, а не логики приложения.