Commit f3e5c924 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '217530-improve-bulk-dismissal' into 'master'

Remove item when dismissed on security dashboard if no longer in filter

See merge request gitlab-org/gitlab!43468
parents 1372ea0a 9f17a8b9
...@@ -23,6 +23,9 @@ export default { ...@@ -23,6 +23,9 @@ export default {
popoverTitle() { popoverTitle() {
return n__('1 Issue', '%d Issues', this.numberOfIssues); return n__('1 Issue', '%d Issues', this.numberOfIssues);
}, },
issueBadgeEl() {
return () => this.$refs.issueBadge?.$el;
},
}, },
}; };
</script> </script>
...@@ -33,7 +36,7 @@ export default { ...@@ -33,7 +36,7 @@ export default {
<gl-icon name="issues" class="gl-mr-2" /> <gl-icon name="issues" class="gl-mr-2" />
{{ numberOfIssues }} {{ numberOfIssues }}
</gl-badge> </gl-badge>
<gl-popover ref="popover" :target="() => $refs.issueBadge.$el" triggers="hover" placement="top"> <gl-popover ref="popover" :target="issueBadgeEl" triggers="hover" placement="top">
<template #title> <template #title>
{{ popoverTitle }} {{ popoverTitle }}
</template> </template>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { GlButton, GlFormSelect } from '@gitlab/ui'; import { GlButton, GlFormSelect } from '@gitlab/ui';
import { s__, n__ } from '~/locale'; import { s__, n__ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast'; import toast from '~/vue_shared/plugins/global_toast';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import createFlash from '~/flash';
import vulnerabilityDismiss from '../graphql/vulnerability_dismiss.mutation.graphql'; import vulnerabilityDismiss from '../graphql/vulnerability_dismiss.mutation.graphql';
const REASON_NONE = s__('SecurityReports|[No reason]'); const REASON_NONE = s__('SecurityReports|[No reason]');
...@@ -48,30 +48,46 @@ export default { ...@@ -48,30 +48,46 @@ export default {
this.dismissSelectedVulnerabilities(); this.dismissSelectedVulnerabilities();
}, },
dismissSelectedVulnerabilities() { dismissSelectedVulnerabilities() {
let fulfilledCount = 0;
let rejectedCount = 0;
const promises = this.selectedVulnerabilities.map(vulnerability => const promises = this.selectedVulnerabilities.map(vulnerability =>
this.$apollo.mutate({ this.$apollo
mutation: vulnerabilityDismiss, .mutate({
variables: { id: vulnerability.id, comment: this.dismissalReason }, mutation: vulnerabilityDismiss,
}), variables: { id: vulnerability.id, comment: this.dismissalReason },
})
.then(() => {
fulfilledCount += 1;
this.$emit('vulnerability-updated', vulnerability.id);
})
.catch(() => {
rejectedCount += 1;
}),
); );
Promise.all(promises) Promise.all(promises)
.then(() => { .then(() => {
toast( if (fulfilledCount > 0) {
n__( toast(
'%d vulnerability dismissed', n__('%d vulnerability dismissed', '%d vulnerabilities dismissed', fulfilledCount),
'%d vulnerabilities dismissed', );
this.selectedVulnerabilities.length, }
),
);
this.$emit('deselect-all-vulnerabilities'); if (rejectedCount > 0) {
createFlash({
message: n__(
'SecurityReports|There was an error dismissing %d vulnerability. Please try again later.',
'SecurityReports|There was an error dismissing %d vulnerabilities. Please try again later.',
rejectedCount,
),
});
}
}) })
.catch(() => { .catch(() => {
createFlash( createFlash({
s__('SecurityReports|There was an error dismissing the vulnerabilities.'), message: s__('SecurityReports|There was an error dismissing the vulnerabilities.'),
'alert', });
);
}); });
}, },
}, },
......
...@@ -15,6 +15,7 @@ import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_ba ...@@ -15,6 +15,7 @@ import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_ba
import VulnerabilityCommentIcon from 'ee/security_dashboard/components/vulnerability_comment_icon.vue'; import VulnerabilityCommentIcon from 'ee/security_dashboard/components/vulnerability_comment_icon.vue';
import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type'; import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
import getPrimaryIdentifier from 'ee/vue_shared/security_reports/store/utils/get_primary_identifier'; import getPrimaryIdentifier from 'ee/vue_shared/security_reports/store/utils/get_primary_identifier';
import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
import SecurityScannerAlert from './security_scanner_alert.vue'; import SecurityScannerAlert from './security_scanner_alert.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
...@@ -87,11 +88,19 @@ export default { ...@@ -87,11 +88,19 @@ export default {
}; };
}, },
computed: { computed: {
// This is a workaround to remove vulnerabilities from the list when their state has changed
// through the bulk update feature, but no longer matches the filters. For more details:
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43468#note_420050017
filteredVulnerabilities() {
return this.vulnerabilities.filter(x =>
this.filters.state?.length ? this.filters.state.includes(x.state) : true,
);
},
isSortable() { isSortable() {
return Boolean(this.$listeners['sort-changed']); return Boolean(this.$listeners['sort-changed']);
}, },
hasAnyScannersOtherThanGitLab() { hasAnyScannersOtherThanGitLab() {
return this.vulnerabilities.some(v => v.scanner?.vendor !== 'GitLab'); return this.filteredVulnerabilities.some(v => v.scanner?.vendor !== 'GitLab');
}, },
notEnabledSecurityScanners() { notEnabledSecurityScanners() {
const { available = [], enabled = [] } = this.securityScanners; const { available = [], enabled = [] } = this.securityScanners;
...@@ -112,16 +121,16 @@ export default { ...@@ -112,16 +121,16 @@ export default {
return Object.keys(this.filters).length > 0; return Object.keys(this.filters).length > 0;
}, },
hasSelectedAllVulnerabilities() { hasSelectedAllVulnerabilities() {
if (!this.vulnerabilities.length) { if (!this.filteredVulnerabilities.length) {
return false; return false;
} }
return this.numOfSelectedVulnerabilities === this.vulnerabilities.length; return this.numOfSelectedVulnerabilities === this.filteredVulnerabilities.length;
}, },
numOfSelectedVulnerabilities() { numOfSelectedVulnerabilities() {
return Object.keys(this.selectedVulnerabilities).length; return Object.keys(this.selectedVulnerabilities).length;
}, },
shouldShowSelectionSummary() { shouldShowSelectionSummary() {
return this.shouldShowSelection && Boolean(this.numOfSelectedVulnerabilities); return this.shouldShowSelection && this.numOfSelectedVulnerabilities > 0;
}, },
theadClass() { theadClass() {
return this.shouldShowSelectionSummary ? 'below-selection-summary' : ''; return this.shouldShowSelectionSummary ? 'below-selection-summary' : '';
...@@ -195,8 +204,8 @@ export default { ...@@ -195,8 +204,8 @@ export default {
filters() { filters() {
this.selectedVulnerabilities = {}; this.selectedVulnerabilities = {};
}, },
vulnerabilities(vulnerabilities) { filteredVulnerabilities() {
const ids = new Set(vulnerabilities.map(v => v.id)); const ids = new Set(this.filteredVulnerabilities.map(v => v.id));
Object.keys(this.selectedVulnerabilities).forEach(vulnerabilityId => { Object.keys(this.selectedVulnerabilities).forEach(vulnerabilityId => {
if (!ids.has(vulnerabilityId)) { if (!ids.has(vulnerabilityId)) {
...@@ -219,6 +228,9 @@ export default { ...@@ -219,6 +228,9 @@ export default {
return file; return file;
}, },
deselectVulnerability(vulnerabilityId) {
this.$delete(this.selectedVulnerabilities, vulnerabilityId);
},
deselectAllVulnerabilities() { deselectAllVulnerabilities() {
this.selectedVulnerabilities = {}; this.selectedVulnerabilities = {};
}, },
...@@ -282,6 +294,11 @@ export default { ...@@ -282,6 +294,11 @@ export default {
this.$emit('sort-changed', { ...args, sortBy: convertToSnakeCase(args.sortBy) }); this.$emit('sort-changed', { ...args, sortBy: convertToSnakeCase(args.sortBy) });
} }
}, },
getVulnerabilityState(state = '') {
const stateName = state.toLowerCase();
// Use the raw state name if we don't have a localization for it.
return VULNERABILITY_STATES[stateName] || stateName;
},
}, },
VULNERABILITIES_PER_PAGE, VULNERABILITIES_PER_PAGE,
SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY, SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY,
...@@ -298,12 +315,12 @@ export default { ...@@ -298,12 +315,12 @@ export default {
<selection-summary <selection-summary
v-if="shouldShowSelectionSummary" v-if="shouldShowSelectionSummary"
:selected-vulnerabilities="Object.values(selectedVulnerabilities)" :selected-vulnerabilities="Object.values(selectedVulnerabilities)"
@deselect-all-vulnerabilities="deselectAllVulnerabilities" @vulnerability-updated="deselectVulnerability"
/> />
<gl-table <gl-table
:busy="isLoading" :busy="isLoading"
:fields="fields" :fields="fields"
:items="vulnerabilities" :items="filteredVulnerabilities"
:thead-class="theadClass" :thead-class="theadClass"
:sort-desc="sortDesc" :sort-desc="sortDesc"
:sort-by="sortBy" :sort-by="sortBy"
...@@ -313,6 +330,7 @@ export default { ...@@ -313,6 +330,7 @@ export default {
class="vulnerability-list" class="vulnerability-list"
show-empty show-empty
responsive responsive
primary-key="id"
@sort-changed="handleSortChange" @sort-changed="handleSortChange"
> >
<template #head(checkbox)> <template #head(checkbox)>
...@@ -350,7 +368,7 @@ export default { ...@@ -350,7 +368,7 @@ export default {
</template> </template>
<template #cell(state)="{ item }"> <template #cell(state)="{ item }">
<span class="text-capitalize js-status">{{ item.state.toLowerCase() }}</span> <span class="text-capitalize js-status">{{ getVulnerabilityState(item.state) }}</span>
</template> </template>
<template #cell(severity)="{ item }"> <template #cell(severity)="{ item }">
......
---
title: Remove item when dismissed on security dashboard if it no longer matches filter
merge_request: 43468
author:
type: changed
...@@ -14,7 +14,7 @@ export const generateVulnerabilities = () => [ ...@@ -14,7 +14,7 @@ export const generateVulnerabilities = () => [
], ],
title: 'Vulnerability 0', title: 'Vulnerability 0',
severity: 'critical', severity: 'critical',
state: 'dismissed', state: 'DISMISSED',
reportType: 'SAST', reportType: 'SAST',
resolvedOnDefaultBranch: true, resolvedOnDefaultBranch: true,
location: { location: {
...@@ -39,7 +39,7 @@ export const generateVulnerabilities = () => [ ...@@ -39,7 +39,7 @@ export const generateVulnerabilities = () => [
], ],
title: 'Vulnerability 1', title: 'Vulnerability 1',
severity: 'high', severity: 'high',
state: 'opened', state: 'DETECTED',
reportType: 'DEPENDENCY_SCANNING', reportType: 'DEPENDENCY_SCANNING',
location: { location: {
file: 'src/main/java/com/gitlab/security_products/tests/App.java', file: 'src/main/java/com/gitlab/security_products/tests/App.java',
...@@ -58,7 +58,7 @@ export const generateVulnerabilities = () => [ ...@@ -58,7 +58,7 @@ export const generateVulnerabilities = () => [
identifiers: [], identifiers: [],
title: 'Vulnerability 2', title: 'Vulnerability 2',
severity: 'high', severity: 'high',
state: 'opened', state: 'DETECTED',
reportType: 'CUSTOM_SCANNER_WITHOUT_TRANSLATION', reportType: 'CUSTOM_SCANNER_WITHOUT_TRANSLATION',
location: { location: {
file: 'src/main/java/com/gitlab/security_products/tests/App.java', file: 'src/main/java/com/gitlab/security_products/tests/App.java',
...@@ -74,7 +74,7 @@ export const generateVulnerabilities = () => [ ...@@ -74,7 +74,7 @@ export const generateVulnerabilities = () => [
id: 'id_3', id: 'id_3',
title: 'Vulnerability 3', title: 'Vulnerability 3',
severity: 'high', severity: 'high',
state: 'opened', state: 'DETECTED',
location: { location: {
file: 'yarn.lock', file: 'yarn.lock',
}, },
...@@ -87,7 +87,7 @@ export const generateVulnerabilities = () => [ ...@@ -87,7 +87,7 @@ export const generateVulnerabilities = () => [
id: 'id_4', id: 'id_4',
title: 'Vulnerability 4', title: 'Vulnerability 4',
severity: 'critical', severity: 'critical',
state: 'dismissed', state: 'DISMISSED',
location: {}, location: {},
project: { project: {
nameWithNamespace: 'Administrator / Security reports', nameWithNamespace: 'Administrator / Security reports',
......
...@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'; ...@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils';
import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue'; import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue';
import { GlFormSelect, GlButton } from '@gitlab/ui'; import { GlFormSelect, GlButton } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import createFlash from '~/flash';
import toast from '~/vue_shared/plugins/global_toast'; import toast from '~/vue_shared/plugins/global_toast';
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -25,11 +25,7 @@ describe('Selection Summary component', () => { ...@@ -25,11 +25,7 @@ describe('Selection Summary component', () => {
const dismissButton = () => wrapper.find(GlButton); const dismissButton = () => wrapper.find(GlButton);
const dismissMessage = () => wrapper.find({ ref: 'dismiss-message' }); const dismissMessage = () => wrapper.find({ ref: 'dismiss-message' });
const formSelect = () => wrapper.find(GlFormSelect); const formSelect = () => wrapper.find(GlFormSelect);
const createComponent = ({ props = {}, data = defaultData, mocks = defaultMocks }) => { const createComponent = ({ props = {}, data = defaultData, mocks = defaultMocks } = {}) => {
if (wrapper) {
throw new Error('Please avoid recreating components in the same spec');
}
spyMutate = mocks.$apollo.mutate; spyMutate = mocks.$apollo.mutate;
wrapper = mount(SelectionSummary, { wrapper = mount(SelectionSummary, {
mocks: { mocks: {
...@@ -63,7 +59,7 @@ describe('Selection Summary component', () => { ...@@ -63,7 +59,7 @@ describe('Selection Summary component', () => {
expect(dismissButton().attributes('disabled')).toBe('disabled'); expect(dismissButton().attributes('disabled')).toBe('disabled');
}); });
it('should have the button enabled if a vulnerability is selected and an option is selected', () => { it('should have the button enabled if a vulnerability is selected and an option is selected', async () => {
expect(wrapper.vm.dismissalReason).toBe(null); expect(wrapper.vm.dismissalReason).toBe(null);
expect(wrapper.findAll('option')).toHaveLength(4); expect(wrapper.findAll('option')).toHaveLength(4);
formSelect() formSelect()
...@@ -71,15 +67,15 @@ describe('Selection Summary component', () => { ...@@ -71,15 +67,15 @@ describe('Selection Summary component', () => {
.at(1) .at(1)
.setSelected(); .setSelected();
formSelect().trigger('change'); formSelect().trigger('change');
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
expect(wrapper.vm.dismissalReason).toEqual(expect.any(String));
expect(dismissButton().attributes('disabled')).toBe(undefined); expect(wrapper.vm.dismissalReason).toEqual(expect.any(String));
}); expect(dismissButton().attributes('disabled')).toBe(undefined);
}); });
}); });
}); });
describe('with 1 vulnerabilities selected', () => { describe('with multiple vulnerabilities selected', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }, { id: 'id_1' }] } }); createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }, { id: 'id_1' }] } });
}); });
...@@ -93,10 +89,12 @@ describe('Selection Summary component', () => { ...@@ -93,10 +89,12 @@ describe('Selection Summary component', () => {
let mutateMock; let mutateMock;
beforeEach(() => { beforeEach(() => {
mutateMock = jest.fn().mockResolvedValue(); mutateMock = jest.fn(data =>
data.variables.id % 2 === 0 ? Promise.resolve() : Promise.reject(),
);
createComponent({ createComponent({
props: { selectedVulnerabilities: [{ id: 'id_0' }, { id: 'id_1' }] }, props: { selectedVulnerabilities: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }] },
data: { dismissalReason: 'Will Not Fix' }, data: { dismissalReason: 'Will Not Fix' },
mocks: { $apollo: { mutate: mutateMock } }, mocks: { $apollo: { mutate: mutateMock } },
}); });
...@@ -104,31 +102,29 @@ describe('Selection Summary component', () => { ...@@ -104,31 +102,29 @@ describe('Selection Summary component', () => {
it('should make an API request for each vulnerability', () => { it('should make an API request for each vulnerability', () => {
dismissButton().trigger('submit'); dismissButton().trigger('submit');
expect(spyMutate).toHaveBeenCalledTimes(2); expect(spyMutate).toHaveBeenCalledTimes(5);
}); });
it('should show toast with the right message if all calls were successful', () => { it('should show toast with the right message for the successful calls', async () => {
dismissButton().trigger('submit'); dismissButton().trigger('submit');
return waitForPromises().then(() => { await waitForPromises();
expect(toast).toHaveBeenCalledWith('2 vulnerabilities dismissed');
}); expect(toast).toHaveBeenCalledWith('2 vulnerabilities dismissed');
}); });
it('should show flash with the right message if some calls failed', () => { it('should show flash with the right message for the failed calls', async () => {
mutateMock.mockRejectedValue();
dismissButton().trigger('submit'); dismissButton().trigger('submit');
return waitForPromises().then(() => { await waitForPromises();
expect(createFlash).toHaveBeenCalledWith(
'There was an error dismissing the vulnerabilities.', expect(createFlash).toHaveBeenCalledWith({
'alert', message: 'There was an error dismissing 3 vulnerabilities. Please try again later.',
);
}); });
}); });
}); });
describe('when vulnerabilities are not selected', () => { describe('when vulnerabilities are not selected', () => {
beforeEach(() => { beforeEach(() => {
createComponent({}); createComponent();
}); });
it('should have the button disabled', () => { it('should have the button disabled', () => {
......
...@@ -12,6 +12,7 @@ import VulnerabilityList, { ...@@ -12,6 +12,7 @@ import VulnerabilityList, {
import FiltersProducedNoResults from 'ee/security_dashboard/components/empty_states/filters_produced_no_results.vue'; import FiltersProducedNoResults from 'ee/security_dashboard/components/empty_states/filters_produced_no_results.vue';
import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/empty_states/dashboard_has_no_vulnerabilities.vue'; import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/empty_states/dashboard_has_no_vulnerabilities.vue';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import { capitalize } from 'lodash';
import { generateVulnerabilities, vulnerabilities } from './mock_data'; import { generateVulnerabilities, vulnerabilities } from './mock_data';
describe('Vulnerability list component', () => { describe('Vulnerability list component', () => {
...@@ -19,7 +20,7 @@ describe('Vulnerability list component', () => { ...@@ -19,7 +20,7 @@ describe('Vulnerability list component', () => {
let wrapper; let wrapper;
const createWrapper = ({ props = {}, listeners }) => { const createWrapper = ({ props = {}, listeners } = {}) => {
return mount(VulnerabilityList, { return mount(VulnerabilityList, {
propsData: { propsData: {
vulnerabilities: [], vulnerabilities: [],
...@@ -42,7 +43,9 @@ describe('Vulnerability list component', () => { ...@@ -42,7 +43,9 @@ describe('Vulnerability list component', () => {
const findTable = () => wrapper.find(GlTable); const findTable = () => wrapper.find(GlTable);
const findSortableColumn = () => wrapper.find('[aria-sort="descending"]'); const findSortableColumn = () => wrapper.find('[aria-sort="descending"]');
const findCell = label => wrapper.find(`.js-${label}`); const findCell = label => wrapper.find(`.js-${label}`);
const findRow = (index = 0) => wrapper.findAll('tbody tr').at(index); const findRows = () => wrapper.findAll('tbody tr');
const findRow = (index = 0) => findRows().at(index);
const findRowById = id => wrapper.find(`tbody tr[data-pk="${id}"`);
const findIssuesBadge = () => wrapper.find(IssuesBadge); const findIssuesBadge = () => wrapper.find(IssuesBadge);
const findRemediatedBadge = () => wrapper.find(RemediatedBadge); const findRemediatedBadge = () => wrapper.find(RemediatedBadge);
const findSecurityScannerAlert = () => wrapper.find(SecurityScannerAlert); const findSecurityScannerAlert = () => wrapper.find(SecurityScannerAlert);
...@@ -76,7 +79,7 @@ describe('Vulnerability list component', () => { ...@@ -76,7 +79,7 @@ describe('Vulnerability list component', () => {
it('should correctly render the status', () => { it('should correctly render the status', () => {
const cell = findCell('status'); const cell = findCell('status');
expect(cell.text()).toBe(newVulnerabilities[0].state); expect(cell.text()).toBe(capitalize(newVulnerabilities[0].state));
}); });
it('should correctly render the severity', () => { it('should correctly render the severity', () => {
...@@ -133,12 +136,11 @@ describe('Vulnerability list component', () => { ...@@ -133,12 +136,11 @@ describe('Vulnerability list component', () => {
expect(findSelectionSummary().exists()).toBe(false); expect(findSelectionSummary().exists()).toBe(false);
}); });
it('should show the selection summary when a checkbox is selected', () => { it('should show the selection summary when a checkbox is selected', async () => {
findDataCell('vulnerability-checkbox').setChecked(true); findDataCell('vulnerability-checkbox').setChecked(true);
await wrapper.vm.$nextTick();
return wrapper.vm.$nextTick().then(() => { expect(findSelectionSummary().exists()).toBe(true);
expect(findSelectionSummary().exists()).toBe(true);
});
}); });
it('should sync selected vulnerabilities when the vulnerability list is updated', async () => { it('should sync selected vulnerabilities when the vulnerability list is updated', async () => {
...@@ -154,6 +156,18 @@ describe('Vulnerability list component', () => { ...@@ -154,6 +156,18 @@ describe('Vulnerability list component', () => {
expect(findSelectionSummary().exists()).toBe(false); expect(findSelectionSummary().exists()).toBe(false);
}); });
it('should uncheck a selected vulnerability after the vulnerability is updated', async () => {
const checkbox = () => findDataCell('vulnerability-checkbox');
checkbox().setChecked(true);
expect(checkbox().element.checked).toBe(true);
await wrapper.vm.$nextTick();
findSelectionSummary().vm.$emit('vulnerability-updated', newVulnerabilities[0].id);
await wrapper.vm.$nextTick();
expect(checkbox().element.checked).toBe(false);
});
}); });
describe('when vulnerability selection is disabled', () => { describe('when vulnerability selection is disabled', () => {
...@@ -371,7 +385,7 @@ describe('Vulnerability list component', () => { ...@@ -371,7 +385,7 @@ describe('Vulnerability list component', () => {
describe('with no vulnerabilities when there are no filters', () => { describe('with no vulnerabilities when there are no filters', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createWrapper({}); wrapper = createWrapper();
}); });
it('should show the empty state', () => { it('should show the empty state', () => {
...@@ -398,6 +412,26 @@ describe('Vulnerability list component', () => { ...@@ -398,6 +412,26 @@ describe('Vulnerability list component', () => {
}); });
}); });
describe('with vulnerabilities when there are filters', () => {
it.each`
state
${['DETECTED']}
${['DISMISSED']}
${[]}
${['DETECTED', 'DISMISSED']}
`('should only show vulnerabilities that match filter $state', state => {
wrapper = createWrapper({ props: { vulnerabilities, filters: { state } } });
const filteredVulnerabilities = vulnerabilities.filter(x =>
state.length ? state.includes(x.state) : true,
);
expect(findRows().length).toBe(filteredVulnerabilities.length);
filteredVulnerabilities.forEach(vulnerability => {
expect(findRowById(vulnerability.id).exists()).toBe(true);
});
});
});
describe('security scanner alerts', () => { describe('security scanner alerts', () => {
describe.each` describe.each`
available | enabled | pipelineRun | expectAlertShown available | enabled | pipelineRun | expectAlertShown
...@@ -492,7 +526,7 @@ describe('Vulnerability list component', () => { ...@@ -492,7 +526,7 @@ describe('Vulnerability list component', () => {
describe('when does not have a sort-changed listener defined', () => { describe('when does not have a sort-changed listener defined', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createWrapper({}); wrapper = createWrapper();
}); });
it('is not sortable', () => { it('is not sortable', () => {
......
...@@ -23027,6 +23027,11 @@ msgstr "" ...@@ -23027,6 +23027,11 @@ msgstr ""
msgid "SecurityReports|There was an error deleting the comment." msgid "SecurityReports|There was an error deleting the comment."
msgstr "" msgstr ""
msgid "SecurityReports|There was an error dismissing %d vulnerability. Please try again later."
msgid_plural "SecurityReports|There was an error dismissing %d vulnerabilities. Please try again later."
msgstr[0] ""
msgstr[1] ""
msgid "SecurityReports|There was an error dismissing the vulnerabilities." msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr "" msgstr ""
......
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