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