Commit 0d0b5c9f authored by Savas Vedova's avatar Savas Vedova

Persist filters while navigating

- Sync the url with filters
- Add tests
parent bf0c8164
<script> <script>
import { initFirstClassVulnerabilityFilters, mapProjects } from 'ee/security_dashboard/helpers'; import { isEqual } from 'lodash';
import { ALL } from 'ee/security_dashboard/store/modules/filters/constants'; import { ALL } from 'ee/security_dashboard/store/modules/filters/constants';
import { setFilter } from 'ee/security_dashboard/store/modules/filters/utils'; import { setFilter } from 'ee/security_dashboard/store/modules/filters/utils';
import DashboardFilter from 'ee/security_dashboard/components/filter.vue'; import DashboardFilter from 'ee/security_dashboard/components/filter.vue';
import { initFirstClassVulnerabilityFilters, mapProjects } from 'ee/security_dashboard/helpers';
export default { export default {
components: { components: {
...@@ -16,6 +17,17 @@ export default { ...@@ -16,6 +17,17 @@ export default {
filters: initFirstClassVulnerabilityFilters(this.projects), filters: initFirstClassVulnerabilityFilters(this.projects),
}; };
}, },
computed: {
selectedFilters() {
return this.filters.reduce((acc, { id, selection }) => {
if (!selection.has(ALL)) {
acc[id] = Array.from(selection);
}
return acc;
}, {});
},
},
watch: { watch: {
/** /**
* Initially the project list empty. We fetch them dynamically from GraphQL while * Initially the project list empty. We fetch them dynamically from GraphQL while
...@@ -28,19 +40,38 @@ export default { ...@@ -28,19 +40,38 @@ export default {
projectFilter.options = [projectFilter.options[0], ...mapProjects(this.projects)]; projectFilter.options = [projectFilter.options[0], ...mapProjects(this.projects)];
} }
}, },
'$route.query': {
immediate: true,
handler(newQuery) {
let changed;
this.filters.forEach((filter, i) => {
let urlFilter = newQuery[filter.id];
if (typeof urlFilter === 'undefined') {
urlFilter = [ALL];
} else if (!Array.isArray(urlFilter)) {
urlFilter = [urlFilter];
}
if (isEqual(this.selectedFilters[filter.id], newQuery[filter.id]) === false) {
changed = true;
this.filters[i].selection = new Set(urlFilter);
}
});
if (changed) {
this.$emit('filterChange', this.selectedFilters);
}
},
},
}, },
methods: { methods: {
setFilter(options) { setFilter(options) {
const selectedFilters = {};
this.filters = setFilter(this.filters, options); this.filters = setFilter(this.filters, options);
this.$router.push({ query: this.selectedFilters });
this.filters.forEach(({ id, selection }) => { this.$emit('filterChange', this.selectedFilters);
if (!selection.has(ALL)) {
selectedFilters[id] = Array.from(selection);
}
});
this.$emit('filterChange', selectedFilters);
}, },
}, },
}; };
......
import { shallowMount } from '@vue/test-utils'; import VueRouter from 'vue-router';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { initFirstClassVulnerabilityFilters } from 'ee/security_dashboard/helpers'; import { initFirstClassVulnerabilityFilters } from 'ee/security_dashboard/helpers';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue'; import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import Filter from 'ee/security_dashboard/components/filter.vue'; import Filter from 'ee/security_dashboard/components/filter.vue';
const router = new VueRouter();
const localVue = createLocalVue();
localVue.use(VueRouter);
describe('First class vulnerability filters component', () => { describe('First class vulnerability filters component', () => {
let wrapper; let wrapper;
let filters; let filters;
const projects = [ const projects = [
{ id: 'gid://gitlab/Project/11', name: 'GitLab Org' }, { id: 'gid://gitlab/Project/11', name: 'GitLab Org' },
{ id: 'gid://gitlab/Project/12', name: 'GitLab Com' }, { id: 'gid://gitlab/Project/12', name: 'GitLab Com' },
]; ];
const findFilters = () => wrapper.findAll(Filter); const findFilters = () => wrapper.findAll(Filter);
const findFirstFilter = () => findFilters().at(0); const findStateFilter = () => findFilters().at(0);
const findSeverityFilter = () => findFilters().at(1);
const findReportTypeFilter = () => findFilters().at(2);
const findProjectFilter = () => findFilters().at(3);
const findLastFilter = () => findFilters().at(filters.length - 1); const findLastFilter = () => findFilters().at(filters.length - 1);
const createComponent = ({ propsData } = {}) => { const createComponent = ({ propsData, listeners } = {}) => {
return shallowMount(Filters, { propsData }); return shallowMount(Filters, { localVue, router, propsData, listeners });
}; };
afterEach(() => { afterEach(() => {
...@@ -35,7 +44,7 @@ describe('First class vulnerability filters component', () => { ...@@ -35,7 +44,7 @@ describe('First class vulnerability filters component', () => {
}); });
it('should pass down the filter information to the first filter', () => { it('should pass down the filter information to the first filter', () => {
expect(findFirstFilter().props().filter).toEqual(filters[0]); expect(findStateFilter().props().filter).toEqual(filters[0]);
}); });
it('should call the setFilter mutation when setting a filter', () => { it('should call the setFilter mutation when setting a filter', () => {
...@@ -43,7 +52,7 @@ describe('First class vulnerability filters component', () => { ...@@ -43,7 +52,7 @@ describe('First class vulnerability filters component', () => {
const options = { foo: 'bar' }; const options = { foo: 'bar' };
wrapper.setMethods({ setFilter: stub }); wrapper.setMethods({ setFilter: stub });
findFirstFilter().vm.$emit('setFilter', options); findStateFilter().vm.$emit('setFilter', options);
expect(stub).toHaveBeenCalledWith(options); expect(stub).toHaveBeenCalledWith(options);
}); });
...@@ -99,27 +108,105 @@ describe('First class vulnerability filters component', () => { ...@@ -99,27 +108,105 @@ describe('First class vulnerability filters component', () => {
}); });
}); });
describe('when setFilter is called', () => { describe.each`
let filterId; filter | value | selector
let optionId; ${'state'} | ${'DETECTED,DISMISSED'} | ${findStateFilter}
${'severity'} | ${'MEDIUM'} | ${findSeverityFilter}
${'reportType'} | ${'SAST'} | ${findReportTypeFilter}
${'projectId'} | ${'12'} | ${findProjectFilter}
`('when filters are persisted', ({ filter, value, selector }) => {
describe(`with filter set to ${filter}: ${value}`, () => {
let filterChangeSpy;
beforeEach(() => {
filterChangeSpy = jest.fn();
wrapper = createComponent({
propsData: { projects },
listeners: { filterChange: filterChangeSpy },
});
router.push({ query: { [filter]: value.split(',') } });
});
beforeEach(() => { it(`should have the ${filter} filter as pre-selected`, () => {
filterId = filters[0].id; expect(selector().props('filter').selection).toEqual(new Set(value.split(',')));
optionId = filters[0].options[1].id; });
wrapper = createComponent();
wrapper.vm.setFilter({ filterId, optionId });
});
it('should set the filters locally', () => { it('should emit a filterChange event', () => {
const expectedFilters = initFirstClassVulnerabilityFilters(); expect(wrapper.emitted().filterChange).toBeTruthy();
expectedFilters[0].selection = new Set([optionId]); });
it('should not trigger the filterChange additonally when the filters do not change', () => {
router.push({
query: {
...wrapper.vm.$route.query,
'some-unrelated-query-param': 'true',
},
});
return wrapper.vm.$nextTick(() => {
expect(filterChangeSpy).toHaveBeenCalledTimes(1);
});
});
it('should trigger the filterChange when the filters are reset', () => {
router.push({ query: {} });
return wrapper.vm.$nextTick(() => {
expect(filterChangeSpy).toHaveBeenNthCalledWith(2, {});
});
});
expect(wrapper.vm.filters).toEqual(expectedFilters); it('should reset the filters when the URL contains no more filters', () => {
router.push({ query: {} });
return wrapper.vm.$nextTick(() => {
expect(selector().props('filter').selection).toEqual(new Set(['all']));
});
});
}); });
});
describe.each`
filter | selector | index
${'state'} | ${findStateFilter} | ${0}
${'severity'} | ${findSeverityFilter} | ${1}
${'reportType'} | ${findReportTypeFilter} | ${2}
${'projectId'} | ${findProjectFilter} | ${3}
`('when setFilter is called', ({ filter, selector, index }) => {
describe(filter, () => {
let filterId;
let optionId;
let routePushSpy;
beforeEach(() => {
filters = initFirstClassVulnerabilityFilters(projects);
filterId = filters[index].id;
optionId = filters[index].options[1].id;
wrapper = createComponent({ propsData: { projects } });
routePushSpy = jest.spyOn(router, 'push');
selector().vm.$emit('setFilter', { optionId, filterId });
});
afterEach(() => {
// This will reset the query state
router.push('/');
});
it('should set the filter', () => {
expect(selector().props('filter').selection).toEqual(new Set([optionId]));
});
it('should emit selected filters when a filter is set', () => { it('should emit a filterChange event', () => {
expect(wrapper.emitted().filterChange).toBeTruthy(); expect(wrapper.emitted().filterChange).toBeTruthy();
expect(wrapper.emitted().filterChange[0]).toEqual([{ [filterId]: [optionId] }]); });
it('should update the path', () => {
expect(routePushSpy).toHaveBeenCalledWith({
query: { [filterId]: [optionId] },
});
});
}); });
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment