Kepler.gl построен как надстройка над стеком React + Redux + deck.gl + Mapbox GL, где ключевая логика визуализации распределена между несколькими слоями:
Такая архитектура делает интеграционное тестирование критически важным, поскольку изолированные unit-тесты не отражают поведение системы в реальном сценарии: данные проходят через Redux, трансформируются в визуальные слои и только затем отображаются.
Интеграционные тесты Kepler.gl направлены на проверку связей между:
visState и визуализацией слоёвКлючевая задача — убедиться, что цепочка “данные → состояние → визуализация” работает целостно.
Для тестирования Kepler.gl требуется эмуляция нескольких браузерных и графических API.
Установка зависимостей:
npm install --save-dev jest @testing-library/react @testing-library/jest-dom
npm install --save-dev jest-canvas-mock redux-mock-store
// setupTests.js
import '@testing-library/jest-dom';
import 'jest-canvas-mock';
Kepler.gl активно использует WebGL через deck.gl, поэтому без моков тесты падают.
jest.mock('mapbox-gl', () => ({
Map: () => ({
on: jest.fn(),
remove: jest.fn(),
addControl: jest.fn()
})
}));
HTMLCanvasElement.prototype.getContext = () => {
return {
fillRect: jest.fn(),
clearRect: jest.fn(),
getImageData: jest.fn(),
putImageData: jest.fn(),
createImageData: jest.fn(),
setTransform: jest.fn(),
drawImage: jest.fn()
};
};
Kepler.gl хранит состояние в структуре:
visState — слои, фильтры, взаимодействиеmapState — положение камерыuiState — интерфейсСоздание тестового store:
import { createStore, combineReducers } from 'redux';
import keplerGlReducer from 'kepler.gl/reducers';
const reducers = combineReducers({
keplerGl: keplerGlReducer
});
export function configureTestStore(initialState) {
return createStore(reducers, initialState);
}
Один из ключевых сценариев — проверка добавления датасета и его обработки.
import { addDataToMap } from 'kepler.gl/actions';
const sampleData = {
datasets: {
info: {
label: 'Test Data',
id: 'test'
},
data: {
fields: [
{ name: 'lat', type: 'real' },
{ name: 'lng', type: 'real' }
],
rows: [
[55.75, 37.61],
[59.93, 30.31]
]
}
}
};
test('добавление данных в Kepler.gl store', () => {
const store = configureTestStore();
store.dispatch(addDataToMap(sampleData));
const state = store.getState();
const dataset = state.keplerGl.map.visState.datasets;
expect(Object.keys(dataset)).toContain('test');
});
После загрузки данных Kepler.gl автоматически создаёт слои или позволяет пользователю их настроить.
test('создание слоя после добавления данных', () => {
const store = configureTestStore();
store.dispatch(addDataToMap(sampleData));
const layers = store.getState().keplerGl.map.visState.layers;
expect(layers.length).toBeGreaterThan(0);
expect(layers[0]).toHaveProperty('config');
});
Фильтры — одна из самых сложных частей системы, так как они влияют на GPU-рендеринг через deck.gl.
import { addFilter, setFilter } from 'kepler.gl/actions';
test('добавление и изменение фильтра', () => {
const store = configureTestStore();
store.dispatch(addDataToMap(sampleData));
store.dispatch(addFilter('test'));
store.dispatch(setFilter(0, 'value', [10, 20]));
const filters = store.getState().keplerGl.map.visState.filters;
expect(filters.length).toBe(1);
expect(filters[0].value).toEqual([10, 20]);
});
Kepler.gl синхронизирует положение карты через
mapState.
import { updateMap } from 'kepler.gl/actions';
test('обновление viewport карты', () => {
const store = configureTestStore();
store.dispatch(updateMap({
latitude: 55.75,
longitude: 37.61,
zoom: 10
}));
const mapState = store.getState().keplerGl.map.mapState;
expect(mapState.latitude).toBe(55.75);
expect(mapState.zoom).toBe(10);
});
Интеграционный тест React-компонента включает проверку подключения store и рендера.
import React from 'react';
import { render } from '@testing-library/react';
import KeplerGl from 'kepler.gl';
test('рендер KeplerGl компонента', () => {
const store = configureTestStore();
const { container } = render(
<KeplerGl
id="map"
width={800}
height={600}
mapboxApiAccessToken="test-token"
/>,
{ wrapper: ({ children }) => (
<Provider store={store}>{children}</Provider>
)}
);
expect(container.querySelector('canvas')).toBeTruthy();
});
Слои Kepler.gl могут перекрываться и взаимодействовать через picking system deck.gl.
test('наличие интерактивности слоёв', () => {
const store = configureTestStore();
store.dispatch(addDataToMap(sampleData));
const layers = store.getState().keplerGl.map.visState.layers;
const layer = layers[0];
expect(layer.config).toHaveProperty('visConfig');
expect(layer.config.isVisible).toBe(true);
});
Kepler.gl поддерживает экспорт визуализаций и данных, что также должно тестироваться.
import { exportToJson } from 'kepler.gl/actions';
test('экспорт конфигурации карты', () => {
const store = configureTestStore();
store.dispatch(addDataToMap(sampleData));
const config = store.getState().keplerGl.map;
const exported = exportToJson(config);
expect(exported).toHaveProperty('version');
expect(exported).toHaveProperty('config');
});
Интеграционного уровня тестов часто недостаточно для проверки полной цепочки взаимодействий, поэтому используются E2E инструменты.
describe('Kepler.gl E2E', () => {
it('загрузка данных и отображение слоя', () => {
cy.visit('/');
cy.get('input[type="file"]').attachFile('data.csv');
cy.get('.layer-panel').should('exist');
cy.get('canvas').should('be.visible');
});
});
Kepler.gl может обрабатывать большие наборы данных, поэтому важно контролировать поведение при нагрузке.
test('обработка большого датасета', () => {
const store = configureTestStore();
const largeDataset = {
datasets: {
info: { id: 'big' },
data: {
fields: [{ name: 'lat' }, { name: 'lng' }],
rows: Array.from({ length: 10000 }, (_, i) => [i % 90, i % 180])
}
}
};
store.dispatch(addDataToMap(largeDataset));
const layers = store.getState().keplerGl.map.visState.layers;
expect(layers.length).toBeGreaterThan(0);
});
UI Kepler.gl зависит от синхронизации uiState и
visState.
test('синхронизация UI состояния', () => {
const store = configureTestStore();
store.dispatch({
type: 'TOGGLE_SIDE_PANEL'
});
const uiState = store.getState().keplerGl.map.uiState;
expect(uiState).toBeDefined();
});
Kepler.gl позволяет расширять поведение через плагины и кастомные слои.
test('регистрация кастомного слоя', () => {
const customLayer = {
name: 'CustomLayer',
type: 'custom'
};
const store = configureTestStore();
store.dispatch({
type: 'ADD_LAYER',
layer: customLayer
});
const layers = store.getState().keplerGl.map.visState.layers;
expect(layers.find(l => l.name === 'CustomLayer')).toBeTruthy();
});