Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
* under the License.
*/
import { render, screen } from 'spec/helpers/testing-library';
import { Provider } from 'react-redux';
import { Store } from 'redux';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { stateWithoutNativeFilters } from 'spec/fixtures/mockStore';
import {
ChartCustomizationType,
Expand All @@ -25,7 +29,9 @@ import {
import { FilterBarOrientation } from 'src/dashboard/types';
import FilterControls from './FilterControls';

const mockStore = {
const mockStore = configureStore([thunk]);

const mockStoreForCustomization = {
...stateWithoutNativeFilters,
dashboardInfo: {
...stateWithoutNativeFilters.dashboardInfo,
Expand All @@ -35,7 +41,7 @@ const mockStore = {

jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(selector => selector(mockStore)),
useSelector: jest.fn(selector => selector(mockStoreForCustomization)),
useDispatch: () => jest.fn(),
}));

Expand Down Expand Up @@ -96,9 +102,9 @@ test('renders multiple chart customization dividers in vertical mode', () => {

test('renders chart customization divider in horizontal mode', () => {
const horizontalStore = {
...mockStore,
...mockStoreForCustomization,
dashboardInfo: {
...mockStore.dashboardInfo,
...mockStoreForCustomization.dashboardInfo,
filterBarOrientation: FilterBarOrientation.Horizontal,
},
};
Expand Down Expand Up @@ -184,3 +190,222 @@ test('renders empty state when no chart customizations provided', () => {
container.querySelector('.chart-customization-item-wrapper'),
).not.toBeInTheDocument();
});

const createMockFilter = (id: string, name: string) => ({
id,
name,
filterType: 'filter_select',
targets: [{ datasetId: 1, column: { name: 'country' } }],
defaultDataMask: {},
controlValues: {},
cascadeParentIds: [],
scope: {
rootPath: ['ROOT_ID'],
excluded: [] as string[],
},
isInstant: true,
allowsMultipleValues: true,
isRequired: false,
});

const getDefaultState = (orientation: FilterBarOrientation) => ({
dashboardInfo: {
id: 1,
filterBarOrientation: orientation,
},
dashboardLayout: {
present: {
ROOT_ID: {
type: 'ROOT',
id: 'ROOT_ID',
children: ['TABS-1'],
},
'TABS-1': {
type: 'TABS',
id: 'TABS-1',
children: ['TAB-1', 'TAB-2'],
},
'TAB-1': {
type: 'TAB',
id: 'TAB-1',
children: ['CHART-1'],
},
'TAB-2': {
type: 'TAB',
id: 'TAB-2',
children: ['CHART-2'],
},
'CHART-1': {
type: 'CHART',
id: 'CHART-1',
meta: { chartId: 1 },
},
'CHART-2': {
type: 'CHART',
id: 'CHART-2',
meta: { chartId: 2 },
},
},
},
charts: {
1: { id: 1, formData: {} },
2: { id: 2, formData: {} },
},
dataMask: {},
nativeFilters: {
filters: {
'filter-1': createMockFilter('filter-1', 'Country Filter'),
'filter-2': createMockFilter('filter-2', 'Region Filter'),
'filter-3': createMockFilter('filter-3', 'City Filter'),
},
filterSets: {},
},
dashboardState: {
directPathToChild: [],
activeTabs: ['TAB-1'],
chartCustomizationItems: [],
},
sliceEntities: {
slices: {
1: {
slice_id: 1,
slice_name: 'Chart 1',
form_data: {},
},
2: {
slice_id: 2,
slice_name: 'Chart 2',
form_data: {},
},
},
},
datasources: {},
});

function setupWithFilters(overrideState: any = {}, props: any = {}) {
const state = {
...getDefaultState(FilterBarOrientation.Vertical),
...overrideState,
};
const store = mockStore(state) as Store;

return render(
<Provider store={store}>
<FilterControls
dataMaskSelected={{}}
onFilterSelectionChange={jest.fn()}
onPendingCustomizationDataMaskChange={jest.fn()}
chartCustomizationValues={[]}
{...props}
/>
</Provider>,
);
}

test('FilterControls should mark out-of-scope filters as not overflowed in vertical mode', () => {
const stateWithVertical = getDefaultState(FilterBarOrientation.Vertical);

stateWithVertical.nativeFilters.filters['filter-3'].scope = {
rootPath: ['ROOT_ID'],
excluded: ['TAB-1'],
};

const { container } = setupWithFilters(stateWithVertical);

expect(container).toBeInTheDocument();
});

test('FilterControls should mark out-of-scope filters as overflowed in horizontal mode', () => {
const stateWithHorizontal = getDefaultState(FilterBarOrientation.Horizontal);

stateWithHorizontal.nativeFilters.filters['filter-3'].scope = {
rootPath: ['ROOT_ID'],
excluded: ['TAB-1'],
};

const { container } = setupWithFilters(stateWithHorizontal);

expect(container).toBeInTheDocument();
});

test('FilterControls overflowedByIndex calculation respects filter bar orientation', () => {
const verticalState = getDefaultState(FilterBarOrientation.Vertical);
verticalState.nativeFilters.filters['filter-2'].scope = {
rootPath: ['ROOT_ID'],
excluded: ['TAB-1'],
};

const { rerender, container } = setupWithFilters(verticalState);
expect(container).toBeInTheDocument();

const horizontalState = getDefaultState(FilterBarOrientation.Horizontal);
horizontalState.nativeFilters.filters['filter-2'].scope = {
rootPath: ['ROOT_ID'],
excluded: ['TAB-1'],
};

rerender(
<Provider store={mockStore(horizontalState) as Store}>
<FilterControls
dataMaskSelected={{}}
onFilterSelectionChange={jest.fn()}
onPendingCustomizationDataMaskChange={jest.fn()}
chartCustomizationValues={[]}
/>
</Provider>,
);

expect(container).toBeInTheDocument();
});

test('FilterControls should correctly pass isOverflowing prop to filter controls', () => {
const state = getDefaultState(FilterBarOrientation.Vertical);

state.nativeFilters.filters['filter-1'].scope = {
rootPath: ['ROOT_ID'],
excluded: [],
};

state.nativeFilters.filters['filter-2'].scope = {
rootPath: ['ROOT_ID'],
excluded: ['TAB-1'],
};

const { container } = setupWithFilters(state);

expect(container).toBeInTheDocument();
});

test('FilterControls should handle empty filters list', () => {
const state = getDefaultState(FilterBarOrientation.Vertical);
state.nativeFilters.filters = {} as any;

const { container } = setupWithFilters(state);
expect(container).toBeInTheDocument();
});

test('FilterControls overflowedByIndex updates when filters change scope', () => {
const state = getDefaultState(FilterBarOrientation.Vertical);

const { container, rerender } = setupWithFilters(state);
expect(container).toBeInTheDocument();

const updatedState = getDefaultState(FilterBarOrientation.Vertical);
updatedState.nativeFilters.filters['filter-1'].scope = {
rootPath: ['ROOT_ID'],
excluded: ['TAB-1'],
};

rerender(
<Provider store={mockStore(updatedState) as Store}>
<FilterControls
dataMaskSelected={{}}
onFilterSelectionChange={jest.fn()}
onPendingCustomizationDataMaskChange={jest.fn()}
chartCustomizationValues={[]}
/>
</Provider>,
);

expect(container).toBeInTheDocument();
});
Original file line number Diff line number Diff line change
Expand Up @@ -659,12 +659,19 @@ const FilterControls: FC<FilterControlsProps> = ({
overflowedFiltersInScope.map(({ id }) => id),
);

return filtersWithValues.map(
filter =>
filtersOutOfScopeIds.has(filter.id) ||
overflowedFiltersInScopeIds.has(filter.id),
);
}, [filtersOutOfScope, filtersWithValues, overflowedFiltersInScope]);
return filtersWithValues.map(filter => {
// Out-of-scope filters in vertical mode are in a Collapse panel, not overflowed
if (filtersOutOfScopeIds.has(filter.id)) {
return filterBarOrientation === FilterBarOrientation.Horizontal;
}
return overflowedFiltersInScopeIds.has(filter.id);
});
}, [
filtersOutOfScope,
filtersWithValues,
overflowedFiltersInScope,
filterBarOrientation,
]);

useEffect(() => {
if (outlinedFilterId && overflowedIds.includes(outlinedFilterId)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,46 @@ test('Open and close popover', () => {
expect(defaultProps.onClosePopover).toHaveBeenCalled();
expect(screen.queryByText('Edit time range')).not.toBeInTheDocument();
});

test('DateFilter popover should attach to document.body when not overflowing', () => {
render(setup({ ...defaultProps, isOverflowingFilterBar: false }));

userEvent.click(screen.getByText(NO_TIME_RANGE));

const popover = document.querySelector('.time-range-popover');
expect(popover?.parentElement).toBe(document.body);
});

test('DateFilter popover should attach to parent node when overflowing in filter bar', () => {
render(setup({ ...defaultProps, isOverflowingFilterBar: true }));

userEvent.click(screen.getByText(NO_TIME_RANGE));

const popover = document.querySelector('.time-range-popover');
const trigger = screen.getByTestId(DateFilterTestKey.PopoverOverlay);

expect(popover?.parentElement).toBe(trigger.parentElement);
});

test('DateFilter should properly handle isOverflowingFilterBar prop changes', () => {
const { rerender } = render(
setup({ ...defaultProps, isOverflowingFilterBar: false }),
);

// When not overflowing, popover should attach to document.body
userEvent.click(screen.getByText(NO_TIME_RANGE));
const popover = document.querySelector('.time-range-popover');
expect(popover?.parentElement).toBe(document.body);

userEvent.click(screen.getByText('CANCEL'));

// When overflowing, popover should attach to parent node
rerender(setup({ ...defaultProps, isOverflowingFilterBar: true }));
userEvent.click(screen.getByText(NO_TIME_RANGE));

const popoverAfterRerender = document.querySelector('.time-range-popover');
const trigger = screen.getByTestId(DateFilterTestKey.PopoverOverlay);

expect(popoverAfterRerender?.parentElement).toBe(trigger.parentElement);
expect(popoverAfterRerender?.parentElement).not.toBe(document.body);
});
Loading