[Feature] Dashboard filter indicators (#7908)

* dashboard filter indicators

* add/fix unit tests
This commit is contained in:
Grace Guo
2019-08-21 17:34:08 -07:00
committed by GitHub
parent 0fd7364503
commit 40776bd547
74 changed files with 2455 additions and 454 deletions

View File

@@ -67,7 +67,7 @@ describe('DashboardBuilder', () => {
setColorSchemeAndUnsavedChanges() {},
colorScheme: undefined,
handleComponentDrop() {},
toggleBuilderPane() {},
setDirectPathToChild: sinon.spy(),
};
function setup(overrideProps, useProvider = false, store = mockStore) {
@@ -171,7 +171,7 @@ describe('DashboardBuilder', () => {
expect(wrapper.find(BuilderComponentPane)).toHaveLength(1);
});
it('should change tabs if a top-level Tab is clicked', () => {
it('should change redux state if a top-level Tab is clicked', () => {
const wrapper = setup(
{ dashboardLayout: layoutWithTabs },
true,
@@ -185,6 +185,6 @@ describe('DashboardBuilder', () => {
.at(1)
.simulate('click');
expect(wrapper.find(TabContainer).prop('activeKey')).toBe(1);
expect(props.setDirectPathToChild.callCount).toBe(1);
});
});

View File

@@ -46,6 +46,7 @@ describe('Dashboard', () => {
dashboardState,
dashboardInfo,
charts: chartQueries,
filters: {},
slices: sliceEntities.slices,
datasources,
layout: dashboardLayout.present,
@@ -60,27 +61,23 @@ describe('Dashboard', () => {
return wrapper;
}
const OVERRIDE_FILTERS = {
1: { region: [] },
2: { country_name: ['USA'] },
3: { region: [], country_name: ['USA'] },
};
it('should render a DashboardBuilder', () => {
const wrapper = setup();
expect(wrapper.find(DashboardBuilder)).toHaveLength(1);
});
describe('refreshExcept', () => {
const overrideDashboardState = {
...dashboardState,
filters: {
1: { region: [] },
2: { country_name: ['USA'] },
3: { region: [], country_name: ['USA'] },
},
refresh: true,
};
const overrideDashboardInfo = {
...dashboardInfo,
metadata: {
...dashboardInfo.metadata,
filter_immune_slice_fields: { [chartQueries[chartId].id]: ['region'] },
filterImmuneSliceFields: { [chartQueries[chartId].id]: ['region'] },
},
};
@@ -108,14 +105,14 @@ describe('Dashboard', () => {
expect(spy.callCount).toBe(Object.keys(overrideCharts).length - 1);
});
it('should not call triggerQuery for filter_immune_slices', () => {
it('should not call triggerQuery for filterImmuneSlices', () => {
const wrapper = setup({
charts: overrideCharts,
dashboardInfo: {
...dashboardInfo,
metadata: {
...dashboardInfo.metadata,
filter_immune_slices: Object.keys(overrideCharts).map(id =>
filterImmuneSlices: Object.keys(overrideCharts).map(id =>
Number(id),
),
},
@@ -127,9 +124,9 @@ describe('Dashboard', () => {
expect(spy.callCount).toBe(0);
});
it('should not call triggerQuery for filter_immune_slice_fields', () => {
it('should not call triggerQuery for filterImmuneSliceFields', () => {
const wrapper = setup({
dashboardState: overrideDashboardState,
filters: OVERRIDE_FILTERS,
dashboardInfo: overrideDashboardInfo,
});
const spy = sinon.spy(props.actions, 'triggerQuery');
@@ -140,7 +137,7 @@ describe('Dashboard', () => {
it('should call triggerQuery if filter has more filter-able fields', () => {
const wrapper = setup({
dashboardState: overrideDashboardState,
filters: OVERRIDE_FILTERS,
dashboardInfo: overrideDashboardInfo,
});
const spy = sinon.spy(props.actions, 'triggerQuery');
@@ -187,117 +184,78 @@ describe('Dashboard', () => {
});
describe('componentDidUpdate', () => {
const overrideDashboardState = {
...dashboardState,
filters: {
1: { region: [] },
2: { country_name: ['USA'] },
},
refresh: true,
};
let wrapper;
let prevProps;
let refreshExceptSpy;
it('should not call refresh when there is no change', () => {
const wrapper = setup({ dashboardState: overrideDashboardState });
const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
const prevProps = wrapper.instance().props;
beforeEach(() => {
wrapper = setup({ filters: OVERRIDE_FILTERS });
wrapper.instance().appliedFilters = OVERRIDE_FILTERS;
prevProps = wrapper.instance().props;
refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
});
afterEach(() => {
refreshExceptSpy.restore();
});
it('should not call refresh when is editMode', () => {
wrapper.setProps({
dashboardState: {
...overrideDashboardState,
...dashboardState,
editMode: true,
},
});
wrapper.instance().componentDidUpdate(prevProps);
refreshExceptSpy.restore();
expect(refreshExceptSpy.callCount).toBe(0);
});
it('should not call refresh when there is no change', () => {
wrapper.setProps({
filters: OVERRIDE_FILTERS,
});
wrapper.instance().componentDidUpdate(prevProps);
expect(refreshExceptSpy.callCount).toBe(0);
expect(wrapper.instance().appliedFilters).toBe(OVERRIDE_FILTERS);
});
it('should call refresh if a filter is added', () => {
const wrapper = setup({ dashboardState: overrideDashboardState });
const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
const newFilter = {
gender: ['boy', 'girl'],
};
wrapper.setProps({
dashboardState: {
...overrideDashboardState,
filters: {
...overrideDashboardState.filters,
3: { another_filter: ['please'] },
},
filters: {
...OVERRIDE_FILTERS,
...newFilter,
},
});
refreshExceptSpy.restore();
expect(refreshExceptSpy.callCount).toBe(1);
expect(wrapper.instance().appliedFilters).toEqual({
...OVERRIDE_FILTERS,
...newFilter,
});
});
it('should call refresh if a filter is removed', () => {
const wrapper = setup({ dashboardState: overrideDashboardState });
const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
wrapper.setProps({
dashboardState: {
...overrideDashboardState,
filters: {},
},
filters: {},
});
refreshExceptSpy.restore();
expect(refreshExceptSpy.callCount).toBe(1);
expect(wrapper.instance().appliedFilters).toEqual({});
});
it('should call refresh if a filter is changed', () => {
const wrapper = setup({ dashboardState: overrideDashboardState });
const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
wrapper.setProps({
dashboardState: {
...overrideDashboardState,
filters: {
...overrideDashboardState.filters,
2: { country_name: ['Canada'] },
},
filters: {
...OVERRIDE_FILTERS,
region: ['Canada'],
},
});
refreshExceptSpy.restore();
expect(refreshExceptSpy.callCount).toBe(1);
});
it('should not call refresh if filters change and refresh is false', () => {
const wrapper = setup({ dashboardState: overrideDashboardState });
const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
wrapper.setProps({
dashboardState: {
...overrideDashboardState,
filters: {
...overrideDashboardState.filters,
2: { country_name: ['Canada'] },
},
refresh: false,
},
expect(wrapper.instance().appliedFilters).toEqual({
...OVERRIDE_FILTERS,
region: ['Canada'],
});
refreshExceptSpy.restore();
expect(refreshExceptSpy.callCount).toBe(0);
});
it('should not refresh filter_immune_slices', () => {
const wrapper = setup({
dashboardState: overrideDashboardState,
dashboardInfo: {
...dashboardInfo,
metadata: {
...dashboardInfo.metadata,
filter_immune_slices: [chartId],
},
},
});
const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
const prevProps = wrapper.instance().props;
wrapper.setProps({
dashboardState: {
...overrideDashboardState,
filters: {
...overrideDashboardState.filters,
2: { country_name: ['Canada'] },
},
refresh: false,
},
});
wrapper.instance().componentDidUpdate(prevProps);
refreshExceptSpy.restore();
expect(refreshExceptSpy.callCount).toBe(0);
});
});
});

View File

@@ -0,0 +1,50 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { dashboardFilters } from '../fixtures/mockDashboardFilters';
import { filterId, column } from '../fixtures/mockSliceEntities';
import FilterIndicatorGroup from '../../../../src/dashboard/components/FilterIndicatorGroup';
import FilterBadgeIcon from '../../../../src/components/FilterBadgeIcon';
describe('FilterIndicatorGroup', () => {
const mockedProps = {
indicators: [
{
...dashboardFilters[filterId],
colorCode: 'badge-1',
name: column,
values: ['a', 'b', 'c'],
},
],
setDirectPathToChild: () => {},
};
function setup(overrideProps) {
return shallow(
<FilterIndicatorGroup {...mockedProps} {...overrideProps} />,
);
}
it('should show indicator group with badge', () => {
const wrapper = setup();
expect(wrapper.find(FilterBadgeIcon)).toHaveLength(1);
});
});

View File

@@ -0,0 +1,43 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import FilterIndicatorTooltip from '../../../../src/dashboard/components/FilterIndicatorTooltip';
describe('FilterIndicatorTooltip', () => {
const label = 'region';
const mockedProps = {
colorCode: 'badge-1',
label,
values: [],
clickIconHandler: jest.fn(),
};
function setup(overrideProps) {
return shallow(
<FilterIndicatorTooltip {...mockedProps} {...overrideProps} />,
);
}
it('should show label', () => {
const wrapper = setup();
expect(wrapper.find(`[htmlFor="filter-tooltip-${label}"]`)).toHaveLength(1);
});
});

View File

@@ -0,0 +1,58 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { dashboardFilters } from '../fixtures/mockDashboardFilters';
import { filterId, column } from '../fixtures/mockSliceEntities';
import FilterIndicator from '../../../../src/dashboard/components/FilterIndicator';
import FilterBadgeIcon from '../../../../src/components/FilterBadgeIcon';
describe('FilterIndicator', () => {
const mockedProps = {
indicator: {
...dashboardFilters[filterId],
colorCode: 'badge-1',
name: column,
label: column,
values: ['a', 'b', 'c'],
},
setDirectPathToChild: jest.fn(),
};
function setup(overrideProps) {
return shallow(<FilterIndicator {...mockedProps} {...overrideProps} />);
}
it('should show indicator with badge', () => {
const wrapper = setup();
expect(wrapper.find(FilterBadgeIcon)).toHaveLength(1);
});
it('should call setDirectPathToChild prop', () => {
const wrapper = setup();
const badge = wrapper.find('.filter-indicator');
expect(badge).toHaveLength(1);
badge.simulate('click');
expect(mockedProps.setDirectPathToChild).toHaveBeenCalledWith(
dashboardFilters[filterId].directPathToFilter,
);
});
});

View File

@@ -0,0 +1,74 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { dashboardFilters } from '../fixtures/mockDashboardFilters';
import { filterId, column } from '../fixtures/mockSliceEntities';
import FilterIndicatorsContainer from '../../../../src/dashboard/components/FilterIndicatorsContainer';
import FilterIndicator from '../../../../src/dashboard/components/FilterIndicator';
import * as colorMap from '../../../../src/dashboard/util/dashboardFiltersColorMap';
describe('FilterIndicatorsContainer', () => {
const chartId = 1;
const mockedProps = {
dashboardFilters,
chartId,
chartStatus: 'success',
filterImmuneSlices: [],
filterImmuneSliceFields: {},
setDirectPathToChild: () => {},
};
colorMap.getFilterColorKey = jest.fn(() => 'id_column');
colorMap.getFilterColorMap = jest.fn(() => ({
id_column: 'badge-1',
}));
function setup(overrideProps) {
return shallow(
<FilterIndicatorsContainer {...mockedProps} {...overrideProps} />,
);
}
it('should not show indicator when chart is loading', () => {
const wrapper = setup({ chartStatus: 'loading' });
expect(wrapper.find(FilterIndicator)).toHaveLength(0);
});
it('should not show indicator for filter_box itself', () => {
const wrapper = setup({ chartId: filterId });
expect(wrapper.find(FilterIndicator)).toHaveLength(0);
});
it('should not show indicator when chart is immune', () => {
const wrapper = setup({ filterImmuneSlices: [chartId] });
expect(wrapper.find(FilterIndicator)).toHaveLength(0);
});
it('should not show indicator when chart field is immune', () => {
const wrapper = setup({ filterImmuneSliceFields: { [chartId]: [column] } });
expect(wrapper.find(FilterIndicator)).toHaveLength(0);
});
it('should show indicator', () => {
const wrapper = setup();
expect(wrapper.find(FilterIndicator)).toHaveLength(1);
});
});

View File

@@ -0,0 +1,70 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { Overlay, Tooltip } from 'react-bootstrap';
import FilterTooltipWrapper from '../../../../src/dashboard/components/FilterTooltipWrapper';
import FilterIndicatorTooltip from '../../../../src/dashboard/components/FilterIndicatorTooltip';
describe('FilterTooltipWrapper', () => {
const mockedProps = {
tooltip: (
<FilterIndicatorTooltip
label="region"
values={['a', 'b', 'c']}
clickIconHandler={jest.fn()}
/>
),
};
function setup() {
return shallow(
<FilterTooltipWrapper {...mockedProps}>
<div className="badge-1" />
</FilterTooltipWrapper>,
);
}
it('should contain Overlay and Tooltip', () => {
const wrapper = setup();
expect(wrapper.find(Overlay)).toHaveLength(1);
expect(wrapper.find(Tooltip)).toHaveLength(1);
});
it('should show tooltip on hover', () => {
const wrapper = setup();
wrapper.instance().isHover = true;
jest.useFakeTimers();
wrapper.find('.indicator-container').simulate('mouseover');
jest.runAllTimers();
expect(wrapper.state('show')).toBe(true);
});
it('should hide tooltip on hover', () => {
const wrapper = setup();
wrapper.instance().isHover = false;
jest.useFakeTimers();
wrapper.find('.indicator-container').simulate('mouseout');
jest.runAllTimers();
expect(wrapper.state('show')).toBe(false);
});
});

View File

@@ -52,7 +52,6 @@ describe('Header', () => {
setEditMode: () => {},
showBuilderPane: () => {},
builderPaneType: BUILDER_PANE_TYPE.NONE,
toggleBuilderPane: () => {},
updateCss: () => {},
hasUnsavedChanges: false,
maxUndoHistoryExceeded: false,

View File

@@ -25,17 +25,14 @@ import SliceHeader from '../../../../../src/dashboard/components/SliceHeader';
import ChartContainer from '../../../../../src/chart/ChartContainer';
import mockDatasource from '../../../../fixtures/mockDatasource';
import {
sliceEntitiesForChart as sliceEntities,
sliceId,
} from '../../fixtures/mockSliceEntities';
import { sliceEntitiesForChart as sliceEntities } from '../../fixtures/mockSliceEntities';
import chartQueries, {
sliceId as queryId,
} from '../../fixtures/mockChartQueries';
describe('Chart', () => {
const props = {
id: sliceId,
id: queryId,
width: 100,
height: 100,
updateSliceName() {},
@@ -43,12 +40,12 @@ describe('Chart', () => {
// from redux
chart: chartQueries[queryId],
formData: chartQueries[queryId].formData,
datasource: mockDatasource[sliceEntities.slices[sliceId].datasource],
datasource: mockDatasource[sliceEntities.slices[queryId].datasource],
slice: {
...sliceEntities.slices[sliceId],
...sliceEntities.slices[queryId],
description_markeddown: 'markdown',
},
sliceName: sliceEntities.slices[sliceId].slice_name,
sliceName: sliceEntities.slices[queryId].slice_name,
timeout: 60,
filters: {},
refreshChart() {},
@@ -92,10 +89,10 @@ describe('Chart', () => {
expect(refreshChart.callCount).toBe(1);
});
it('should call addFilter when ChartContainer calls addFilter', () => {
const addFilter = sinon.spy();
const wrapper = setup({ addFilter });
wrapper.instance().addFilter();
expect(addFilter.callCount).toBe(1);
it('should call changeFilter when ChartContainer calls changeFilter', () => {
const changeFilter = sinon.spy();
const wrapper = setup({ changeFilter });
wrapper.instance().changeFilter();
expect(changeFilter.callCount).toBe(1);
});
});