Commit 5e77fecf authored by Alexander Turinske's avatar Alexander Turinske

Update selection summary to emit refetch/deselect

- following Vue best practices, instead of passing a function
  use emit to call a function for refetching of vulnerabilities
  and deselecting vulnerabilities
- update the mutation to automatically update graphql
  cache with id
- use a namespace for small strings
- add .js-no-auto-disable for the dismiss button
- use GlNewButton
- Update selection summary tests
parent 274aad49
<script> <script>
import { s__, __, n__ } from '~/locale'; import { s__, n__ } from '~/locale';
import { GlDeprecatedButton, GlFormSelect } from '@gitlab/ui'; import { GlNewButton, GlFormSelect } from '@gitlab/ui';
import toast from '~/vue_shared/plugins/global_toast'; import toast from '~/vue_shared/plugins/global_toast';
import createFlash from '~/flash'; import createFlash from '~/flash';
import dismissVulnerability from '../graphql/dismissVulnerability.graphql'; import dismissVulnerability from '../graphql/dismissVulnerability.graphql';
const REASON_NONE = __('[No reason]'); const REASON_NONE = s__('Security Reports|[No reason]');
const REASON_WONT_FIX = __("Won't fix / Accept risk"); const REASON_WONT_FIX = s__("Security Reports|Won't fix / Accept risk");
const REASON_FALSE_POSITIVE = __('False positive'); const REASON_FALSE_POSITIVE = s__('Security Reports|False positive');
export default { export default {
name: 'SelectionSummary', name: 'SelectionSummary',
components: { components: {
GlDeprecatedButton, GlNewButton,
GlFormSelect, GlFormSelect,
}, },
props: { props: {
refetchVulnerabilities: {
type: Function,
required: true,
},
deselectAllVulnerabilities: {
type: Function,
required: true,
},
selectedVulnerabilities: { selectedVulnerabilities: {
type: Array, type: Array,
required: true, required: true,
}, },
}, },
data: () => ({ data() {
dismissalReason: null, return {
}), dismissalReason: null,
};
},
computed: { computed: {
selectedVulnerabilitiesCount() { selectedVulnerabilitiesCount() {
return this.selectedVulnerabilities.length; return this.selectedVulnerabilities.length;
...@@ -61,6 +55,7 @@ export default { ...@@ -61,6 +55,7 @@ export default {
this.dismissSelectedVulnerabilities(); this.dismissSelectedVulnerabilities();
}, },
dismissSelectedVulnerabilities() { dismissSelectedVulnerabilities() {
// TODO: Batch vulnerability dismissal with https://gitlab.com/gitlab-org/gitlab/-/issues/214376
const promises = this.selectedVulnerabilities.map(vulnerability => const promises = this.selectedVulnerabilities.map(vulnerability =>
this.$apollo.mutate({ this.$apollo.mutate({
mutation: dismissVulnerability, mutation: dismissVulnerability,
...@@ -71,21 +66,19 @@ export default { ...@@ -71,21 +66,19 @@ export default {
Promise.all(promises) Promise.all(promises)
.then(() => { .then(() => {
toast(this.dismissalSuccessMessage()); toast(this.dismissalSuccessMessage());
this.deselectAllVulnerabilities(); this.$emit('deselect-all-vulnerabilities');
this.$emit('refetch-vulnerabilities');
}) })
.catch(() => { .catch(() => {
createFlash( createFlash(
s__('Security Reports|There was an error dismissing the vulnerabilities.'), s__('Security Reports|There was an error dismissing the vulnerabilities.'),
'alert', 'alert',
); );
})
.finally(() => {
this.refetchVulnerabilities();
}); });
}, },
}, },
dismissalReasons: [ dismissalReasons: [
{ value: null, text: __('Select a reason') }, { value: null, text: s__('Security Reports|Select a reason') },
REASON_FALSE_POSITIVE, REASON_FALSE_POSITIVE,
REASON_WONT_FIX, REASON_WONT_FIX,
REASON_NONE, REASON_NONE,
...@@ -96,15 +89,21 @@ export default { ...@@ -96,15 +89,21 @@ export default {
<template> <template>
<div class="card"> <div class="card">
<form class="card-body d-flex align-items-center" @submit.prevent="handleDismiss"> <form class="card-body d-flex align-items-center" @submit.prevent="handleDismiss">
<span>{{ message }}</span> <span ref="dismiss-message">{{ message }}</span>
<gl-form-select <gl-form-select
v-model="dismissalReason" v-model="dismissalReason"
class="mx-3 w-auto" class="mx-3 w-auto"
:options="$options.dismissalReasons" :options="$options.dismissalReasons"
/> />
<gl-deprecated-button type="submit" variant="close" :disabled="!canDismissVulnerability"> <gl-new-button
{{ __('Dismiss Selected') }} type="submit"
</gl-deprecated-button> class="js-no-auto-disable"
category="secondary"
variant="warning"
:disabled="!canDismissVulnerability"
>
{{ s__('Security Reports|Dismiss Selected') }}
</gl-new-button>
</form> </form>
</div> </div>
</template> </template>
mutation ($id: ID!, $comment: String! ) { mutation ($id: ID!, $comment: String! ) {
dismissVulnerability(input: {id: $id, comment: $comment}) { dismissVulnerability(input: {id: $id, comment: $comment}) {
errors errors
vulnerability {
id
state
}
} }
} }
import { mount } from '@vue/test-utils'; 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, GlNewButton } from '@gitlab/ui';
import createFlash from '~/flash';
import toast from '~/vue_shared/plugins/global_toast';
jest.mock('~/flash');
jest.mock('~/vue_shared/plugins/global_toast');
describe('Selection Summary component', () => { describe('Selection Summary component', () => {
let wrapper; let wrapper;
let spyMutate;
const defaultData = { const defaultData = {
dismissalReason: null, dismissalReason: null,
}; };
const createComponent = ({ props = {}, data = defaultData }) => { const defaultMocks = {
$apollo: {
mutate: jest.fn().mockResolvedValue(),
},
};
const dismissButton = () => wrapper.find(GlNewButton);
const dismissMessage = () => wrapper.find({ ref: 'dismiss-message' });
const formSelect = () => wrapper.find(GlFormSelect);
const createComponent = ({ props = {}, data = defaultData, mocks = defaultMocks }) => {
spyMutate = mocks.$apollo.mutate;
wrapper = mount(SelectionSummary, { wrapper = mount(SelectionSummary, {
mocks: {
...defaultMocks,
...mocks,
},
propsData: { propsData: {
refetchVulnerabilities: jest.fn(),
deselectAllVulnerabilities: jest.fn(),
selectedVulnerabilities: [], selectedVulnerabilities: [],
...props, ...props,
}, },
...@@ -20,102 +40,96 @@ describe('Selection Summary component', () => { ...@@ -20,102 +40,96 @@ describe('Selection Summary component', () => {
}); });
}; };
beforeEach(() => {
createComponent({});
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('computed', () => { describe('when vulnerabilities are selected', () => {
describe('selectedVulnerabilitiesCount', () => { describe('it renders correctly', () => {
it('returns the length if this.selectedVulnerabilities is empty', () => { beforeEach(() => {
expect(wrapper.vm.selectedVulnerabilitiesCount).toBe(0);
});
it('returns the length if this.selectedVulnerabilities is not empty', () => {
createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }] } }); createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }] } });
expect(wrapper.vm.selectedVulnerabilitiesCount).toBe(1);
}); });
});
describe('canDismissVulnerability', () => { it('returns the right message for one selected vulnerabilities', () => {
it('returns true if there is a dismissal reason and a selectedVulnerabilitiesCount greater than zero', () => { expect(dismissMessage().text()).toBe('Dismiss 1 selected vulnerability as');
createComponent({
props: { selectedVulnerabilities: [{ id: 'id_0' }] },
data: { dismissalReason: 'Will Not Fix' },
});
expect(wrapper.vm.canDismissVulnerability).toBe(true);
}); });
it('returns false if there is a dismissal reason and not a selectedVulnerabilitiesCount greater than zero', () => { it('returns the right message for greater than one selected vulnerabilities', () => {
createComponent({ createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }, { id: 'id_1' }] } });
props: { selectedVulnerabilities: [] }, expect(dismissMessage().text()).toBe('Dismiss 2 selected vulnerabilities as');
data: { dismissalReason: 'Will Not Fix' },
});
expect(wrapper.vm.canDismissVulnerability).toBe(false);
}); });
});
it('returns false if there is not a dismissal reason and a selectedVulnerabilitiesCount greater than zero', () => { describe('dismiss button', () => {
beforeEach(() => {
createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }] } }); createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }] } });
expect(wrapper.vm.canDismissVulnerability).toBe(false);
}); });
it('returns false if there is not a dismissal reason and not a selectedVulnerabilitiesCount greater than zero', () => { it('should have the button disabled if an option is not selected', () => {
expect(wrapper.vm.canDismissVulnerability).toBe(false); expect(dismissButton().attributes('disabled')).toBe('disabled');
});
});
describe('message', () => {
it('returns the right message for zero selected vulnerabilities', () => {
expect(wrapper.vm.message).toBe('Dismiss 0 selected vulnerabilities as');
}); });
it('returns the right message for one selected vulnerabilities', () => { it('should have the button enabled if a vulnerability is selected and an option is selected', () => {
createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }] } }); createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }] } });
expect(wrapper.vm.message).toBe('Dismiss 1 selected vulnerability as'); expect(wrapper.vm.dismissalReason).toBe(null);
}); expect(wrapper.findAll('option').length).toBe(4);
formSelect()
it('returns the right message for greater than one selected vulnerabilities', () => { .findAll('option')
createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }, { id: 'id_1' }] } }); .at(1)
expect(wrapper.vm.message).toBe('Dismiss 2 selected vulnerabilities as'); .setSelected();
formSelect().trigger('change');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.dismissalReason).toEqual(expect.any(String));
expect(dismissButton().attributes('disabled')).toBe(undefined);
});
}); });
}); });
});
describe('methods', () => { describe('clicking the dismiss vulnerability button', () => {
describe('getSuccessMessage', () => { beforeEach(() => {
it('returns the right message for zero selected vulnerabilities', () => { createComponent({
expect(wrapper.vm.dismissalSuccessMessage()).toBe('0 vulnerabilities dismissed'); props: { selectedVulnerabilities: [{ id: 'id_0' }, { id: 'id_1' }] },
data: { dismissalReason: 'Will Not Fix' },
});
}); });
it('returns the right message for one selected vulnerabilities', () => { it('should make an API request for each vulnerability', () => {
createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }] } }); dismissButton().trigger('submit');
expect(wrapper.vm.dismissalSuccessMessage()).toBe('1 vulnerability dismissed'); expect(spyMutate).toHaveBeenCalledTimes(2);
}); });
it('returns the right message for greater than one selected vulnerabilities', () => { it('should show toast with the right message if all calls were successful', () => {
createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }, { id: 'id_1' }] } }); dismissButton().trigger('submit');
expect(wrapper.vm.dismissalSuccessMessage()).toBe('2 vulnerabilities dismissed'); setImmediate(() => {
// return wrapper.vm.$nextTick().then(() => {
expect(toast).toHaveBeenCalledWith('2 vulnerabilities dismissed');
});
}); });
});
describe('handleDismiss', () => { it('should show flash with the right message if some calls failed', () => {
it('does call dismissSelectedVulnerabilities when canDismissVulnerability is true', () => {
createComponent({ createComponent({
props: { selectedVulnerabilities: [{ id: 'id_0' }] }, props: { selectedVulnerabilities: [{ id: 'id_0' }, { id: 'id_1' }] },
data: { dismissalReason: 'Will Not Fix' }, data: { dismissalReason: 'Will Not Fix' },
mocks: { $apollo: { mutate: jest.fn().mockRejectedValue() } },
});
dismissButton().trigger('submit');
setImmediate(() => {
expect(createFlash).toHaveBeenCalledWith(
'There was an error dismissing the vulnerabilities.',
'alert',
);
}); });
const spy = jest.spyOn(wrapper.vm, 'dismissSelectedVulnerabilities').mockImplementation();
wrapper.vm.handleDismiss();
expect(spy).toHaveBeenCalled();
}); });
});
});
it('does not call dismissSelectedVulnerabilities when canDismissVulnerability is false', () => { describe('when vulnerabilities are not selected', () => {
const spy = jest.spyOn(wrapper.vm, 'dismissSelectedVulnerabilities'); beforeEach(() => {
wrapper.vm.handleDismiss(); createComponent({});
expect(spy).not.toHaveBeenCalled(); });
}); it('should have the button disabled', () => {
expect(dismissButton().attributes().disabled).toBe('disabled');
}); });
}); });
}); });
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