Redux-архитектура в Kepler.gl строится вокруг предсказуемого состояния, где все изменения проходят через actions и reducers. Это делает тестирование логики ключевым этапом обеспечения стабильности визуализации, конфигурации слоёв и взаимодействий с картой.
Основные уровни тестирования Redux-части:
Actions в Kepler.gl представляют собой чистые описания намерений. Их тестирование сводится к проверке корректности структуры возвращаемого объекта.
Типовой action:
export const addLayer = (config) => ({
type: 'ADD_LAYER',
payload: config
});
Тест проверяет:
Пример теста на Jest:
import { addLayer } from 'actions/layers';
describe('addLayer action', () => {
test('should create correct action', () => {
const config = { id: 'layer_1', type: 'point' };
const action = addLayer(config);
expect(action).toEqual({
type: 'ADD_LAYER',
payload: config
});
});
});
Особое внимание уделяется неизменяемости входных данных: action не должен мутировать переданный объект.
Reducers — центральная часть Redux-логики Kepler.gl. Они управляют состоянием слоёв, фильтров, данных и UI-конфигурации.
Ключевые принципы тестирования reducers:
Пример reducer:
const initialState = {
layers: []
};
export default function layersReducer(state = initialState, action) {
switch (action.type) {
case 'ADD_LAYER':
return {
...state,
layers: [...state.layers, action.payload]
};
default:
return state;
}
}
Тестирование:
import layersReducer from 'reducers/layers';
describe('layersReducer', () => {
test('should return initial state', () => {
expect(layersReducer(undefined, {})).toEqual({
layers: []
});
});
test('should handle ADD_LAYER', () => {
const initialState = { layers: [] };
const action = {
type: 'ADD_LAYER',
payload: { id: 'layer_1' }
};
const nextState = layersReducer(initialState, action);
expect(nextState).toEqual({
layers: [{ id: 'layer_1' }]
});
});
test('should not mutate state', () => {
const state = { layers: [] };
const action = {
type: 'ADD_LAYER',
payload: { id: 'layer_1' }
};
const nextState = layersReducer(state, action);
expect(nextState).not.toBe(state);
expect(state.layers.length).toBe(0);
});
});
Kepler.gl использует вложенные структуры: datasets, layers, interactionConfig, mapState. Это требует тестирования не только поверхностных изменений, но и глубокой неизменяемости.
Пример сценария:
const state = {
mapState: {
latitude: 0,
longitude: 0,
zoom: 10
}
};
const action = {
type: 'UPDATE_MAP_VIEW',
payload: {
latitude: 50,
longitude: 30
}
};
Тест:
expect(nextState.mapState).toEqual({
latitude: 50,
longitude: 30,
zoom: 10
});
Проверяется частичное обновление без потери других полей.
Selectors в Kepler.gl используются для вычисления производных данных: подготовка слоёв, фильтрация dataset, преобразование под deck.gl.
Пример selector:
export const getVisibleLayers = (state) =>
state.layers.filter(layer => layer.isVisible);
Тестирование selector:
import { getVisibleLayers } from 'selectors/layers';
describe('getVisibleLayers', () => {
test('should return only visible layers', () => {
const state = {
layers: [
{ id: 1, isVisible: true },
{ id: 2, isVisible: false }
]
};
const result = getVisibleLayers(state);
expect(result).toEqual([
{ id: 1, isVisible: true }
]);
});
});
Selectors часто мемоизируются (reselect), поэтому дополнительно проверяется кэширование результатов при одинаковом входе.
В Kepler.gl присутствуют комбинированные reducers, например:
Они обрабатывают множество типов actions одновременно.
Пример:
test('visState should handle multiple layers actions', () => {
let state = visStateReducer(undefined, {});
state = visStateReducer(state, {
type: 'ADD_LAYER',
payload: { id: 'l1' }
});
state = visStateReducer(state, {
type: 'UPDATE_LAYER_PROPS',
payload: { id: 'l1', props: { opacity: 0.5 } }
});
expect(state.layers[0].opacity).toBe(0.5);
});
Здесь важна последовательность применения actions, так как состояние эволюционирует.
Middleware в Redux-экосистеме Kepler.gl используется для:
Пример middleware:
const loggerMiddleware = store => next => action => {
console.log(action.type);
return next(action);
};
Тестирование middleware:
import configureMockStore from 'redux-mock-store';
const mockStore = configureMockStore([loggerMiddleware]);
test('middleware should pass action through', () => {
const store = mockStore({});
const action = { type: 'TEST_ACTION' };
store.dispatch(action);
const actions = store.getActions();
expect(actions).toEqual([action]);
});
Kepler.gl может использовать thunk для загрузки данных и подготовки слоёв.
Пример async action:
export const loadData = () => async dispatch => {
const data = await fetch('/data.json').then(res => res.json());
dispatch({
type: 'LOAD_DATA_SUCCESS',
payload: data
});
};
Тестирование:
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
const mockStore = configureMockStore([thunk]);
test('loadData dispatches success action', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ points: [] })
})
);
const store = mockStore({});
await store.dispatch(loadData());
const actions = store.getActions();
expect(actions[0]).toEqual({
type: 'LOAD_DATA_SUCCESS',
payload: { points: [] }
});
});
Redux-архитектура Kepler.gl требует строгого соблюдения immutability. Для этого применяются:
Пример:
import deepFreeze from 'deep-freeze';
test('reducer must not mutate state', () => {
const state = deepFreeze({
layers: []
});
const action = {
type: 'ADD_LAYER',
payload: { id: '1' }
};
const nextState = layersReducer(state, action);
expect(nextState.layers).not.toBe(state.layers);
});
Snapshot применяется для сложных структур Kepler.gl, где ручная проверка всех полей затруднена.
test('visState snapshot', () => {
const state = visStateReducer(undefined, {
type: 'INIT'
});
expect(state).toMatchSnapshot();
});
Снимки позволяют отслеживать непреднамеренные изменения структуры layers, filters и interactionConfig.
Kepler.gl объединяет несколько reducers в rootReducer. Проверка взаимодействия между ними важна для стабильности UI.
Пример сценария:
test('combined redux flow', () => {
let state = rootReducer(undefined, { type: '@@INIT' });
state = rootReducer(state, {
type: 'ADD_LAYER',
payload: { id: 'l1' }
});
state = rootReducer(state, {
type: 'SET_FILTER',
payload: { id: 'f1', value: [0, 10] }
});
expect(state.visState.layers.length).toBe(1);
expect(state.visState.filters.length).toBe(1);
});