Commit b087a911 authored by Artur Fedorov's avatar Artur Fedorov

This MR adds new empty state for corpus management

New component for empty state was added to corpus management. Now
empty state looks consistent with other empty states in security
configuration. New empty state contains illustration, link to
documentation and add new corpus functionality.
Add new corpus functionality was refactored and extracted to
separate component for re-usability. Test were adjusted accordingly.
Changing apollo fetching policies to version 3.x options

Changelog: changed
MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82707
EE: true
parent 40383010
<script>
import { GlLoadingIcon, GlLink, GlKeysetPagination } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql';
import EmptyState from 'ee/security_configuration/corpus_management/components/empty_state.vue';
import CorpusTable from 'ee/security_configuration/corpus_management/components/corpus_table.vue';
import CorpusUpload from 'ee/security_configuration/corpus_management/components/corpus_upload.vue';
import CorpusUploadButton from 'ee/security_configuration/corpus_management/components/corpus_upload_button.vue';
import { s__, __ } from '~/locale';
import getCorpusesQuery from '../graphql/queries/get_corpuses.query.graphql';
import deleteCorpusMutation from '../graphql/mutations/delete_corpus.mutation.graphql';
export default {
components: {
EmptyState,
GlLoadingIcon,
GlLink,
GlKeysetPagination,
CorpusTable,
CorpusUpload,
CorpusUploadButton,
},
apollo: {
states: {
......@@ -33,7 +37,7 @@ export default {
},
},
},
inject: ['projectFullPath', 'corpusHelpPath'],
inject: ['emptyStateSvgPath', 'projectFullPath', 'corpusHelpPath'],
data() {
return {
pagination: {
......@@ -56,6 +60,9 @@ export default {
corpuses() {
return this.states?.project.corpuses.nodes || [];
},
hasCorpuses() {
return this.corpuses.length > 0;
},
pageInfo() {
return this.states?.pageInfo || {};
},
......@@ -79,8 +86,11 @@ export default {
beforeCursor: null,
firstPageSize: this.$options.pageSize,
};
this.$apollo.queries.states.setOptions({ fetchPolicy: fetchPolicies.NETWORK_ONLY });
this.$apollo.queries.states.setOptions({ fetchPolicy: fetchPolicies.CACHE_FIRST });
this.$apollo.queries.states.setOptions({
fetchPolicy: fetchPolicies.NETWORK_ONLY,
nextFetchPolicy: fetchPolicies.CACHE_FIRST,
});
},
onDelete(id) {
return this.$apollo
......@@ -116,31 +126,45 @@ export default {
<template>
<div>
<header>
<h4 class="gl-my-5">
{{ this.$options.i18n.header }}
</h4>
<p>
{{ this.$options.i18n.subHeader }}
<gl-link :href="corpusHelpPath">{{ this.$options.i18n.learnMore }}</gl-link>
</p>
</header>
<template v-if="!hasCorpuses">
<gl-loading-icon v-if="isLoading" size="lg" class="gl-py-13" />
<empty-state v-else>
<template #actions>
<corpus-upload-button @corpus-added="fetchCorpuses" />
</template>
</empty-state>
</template>
<div v-else>
<header>
<h4 class="gl-my-5">
{{ this.$options.i18n.header }}
</h4>
<p>
{{ this.$options.i18n.subHeader }}
<gl-link :href="corpusHelpPath">{{ this.$options.i18n.learnMore }}</gl-link>
</p>
</header>
<corpus-upload @corpus-added="fetchCorpuses" />
<corpus-upload>
<template #action>
<corpus-upload-button class="gl-mr-5 gl-ml-auto" @corpus-added="fetchCorpuses" />
</template>
</corpus-upload>
<gl-loading-icon v-if="isLoading" size="lg" class="gl-py-13" />
<template v-else>
<corpus-table :corpuses="corpuses" @delete="onDelete" />
</template>
<gl-loading-icon v-if="isLoading" size="lg" class="gl-py-6" />
<template v-else>
<corpus-table :corpuses="corpuses" @delete="onDelete" />
</template>
<div v-if="hasPagination" class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-keyset-pagination
v-bind="pageInfo"
:prev-text="$options.i18n.previousPage"
:next-text="$options.i18n.nextPage"
@prev="prevPage"
@next="nextPage"
/>
<div v-if="hasPagination" class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-keyset-pagination
v-bind="pageInfo"
:prev-text="$options.i18n.previousPage"
:next-text="$options.i18n.nextPage"
@prev="prevPage"
@next="nextPage"
/>
</div>
</div>
</div>
</template>
<script>
import { GlButton, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
import { GlSprintf } from '@gitlab/ui';
import { decimalBytes } from '~/lib/utils/unit_format';
import { s__, __ } from '~/locale';
import addCorpusMutation from '../graphql/mutations/add_corpus.mutation.graphql';
import resetCorpus from '../graphql/mutations/reset_corpus.mutation.graphql';
import uploadCorpus from '../graphql/mutations/upload_corpus.mutation.graphql';
import getUploadState from '../graphql/queries/get_upload_state.query.graphql';
import uploadError from '../graphql/mutations/upload_error.mutation.graphql';
import { I18N, MAX_FILE_SIZE } from '../constants';
import CorpusUploadForm from './corpus_upload_form.vue';
import { s__ } from '~/locale';
export default {
components: {
GlSprintf,
GlButton,
GlModal,
CorpusUploadForm,
},
directives: {
GlModalDirective,
},
i18n: {
totalSize: s__('CorpusManagement|Total Size: %{totalSize}'),
newUpload: s__('CorpusManagement|New upload'),
newCorpus: s__('CorpusMnagement|New corpus'),
},
inject: ['projectFullPath', 'canUploadCorpus'],
apollo: {
states: {
query: getUploadState,
update(data) {
return data;
},
},
},
modal: {
actionCancel: {
text: __('Cancel'),
},
modalId: 'corpus-upload-modal',
},
props: {
totalSize: {
......@@ -48,65 +18,9 @@ export default {
},
},
computed: {
queryVariables() {
return {
projectPath: this.projectFullPath,
};
},
formattedFileSize() {
return decimalBytes(this.totalSize);
},
isUploaded() {
return Boolean(this.states?.uploadState.uploadedPackageId);
},
variant() {
return this.isUploaded ? 'confirm' : 'default';
},
actionPrimaryProps() {
return {
text: __('Add'),
attributes: {
'data-testid': 'modal-confirm',
disabled: !this.isUploaded,
variant: this.variant,
},
};
},
},
methods: {
addCorpus() {
return this.$apollo
.mutate({
mutation: addCorpusMutation,
variables: {
name: this.$options.i18n.newCorpus,
projectPath: this.projectFullPath,
packageId: this.states.uploadState.uploadedPackageId,
},
})
.then(() => {
this.$emit('corpus-added');
});
},
resetCorpus() {
this.$apollo.mutate({
mutation: resetCorpus,
variables: { projectPath: this.projectFullPath },
});
},
beginFileUpload({ name, files }) {
if (files[0].size >= MAX_FILE_SIZE) {
this.$apollo.mutate({
mutation: uploadError,
variables: { projectPath: this.projectFullPath, error: I18N.fileTooLarge },
});
} else {
this.$apollo.mutate({
mutation: uploadCorpus,
variables: { name, projectPath: this.projectFullPath, files },
});
}
},
},
};
</script>
......@@ -117,35 +31,10 @@ export default {
<div v-if="totalSize" class="gl-ml-5">
<gl-sprintf :message="$options.i18n.totalSize">
<template #totalSize>
<span class="gl-font-weight-bold">{{ formattedFileSize }}</span>
<span data-testid="total-size" class="gl-font-weight-bold">{{ formattedFileSize }}</span>
</template>
</gl-sprintf>
</div>
<gl-button
v-if="canUploadCorpus"
v-gl-modal-directive="$options.modal.modalId"
data-testid="new-corpus"
class="gl-mr-5 gl-ml-auto"
variant="confirm"
>
{{ $options.i18n.newCorpus }}
</gl-button>
<gl-modal
:modal-id="$options.modal.modalId"
:title="$options.i18n.newCorpus"
size="sm"
:action-primary="actionPrimaryProps"
:action-cancel="$options.modal.actionCancel"
@primary="addCorpus"
@canceled="resetCorpus"
>
<corpus-upload-form
:states="states"
@beginFileUpload="beginFileUpload"
@resetCorpus="resetCorpus"
/>
</gl-modal>
<slot name="action"></slot>
</div>
</template>
<script>
import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import addCorpusMutation from '../graphql/mutations/add_corpus.mutation.graphql';
import resetCorpus from '../graphql/mutations/reset_corpus.mutation.graphql';
import uploadCorpus from '../graphql/mutations/upload_corpus.mutation.graphql';
import getUploadState from '../graphql/queries/get_upload_state.query.graphql';
import uploadError from '../graphql/mutations/upload_error.mutation.graphql';
import { I18N, MAX_FILE_SIZE } from '../constants';
import CorpusUploadForm from './corpus_upload_form.vue';
export default {
components: {
GlButton,
GlModal,
CorpusUploadForm,
},
directives: {
GlModalDirective,
},
i18n: {
newUpload: s__('CorpusManagement|New upload'),
newCorpus: s__('CorpusManagement|New corpus'),
},
inject: ['projectFullPath', 'canUploadCorpus'],
apollo: {
uploadState: {
query: getUploadState,
},
},
modal: {
actionCancel: {
text: __('Cancel'),
},
modalId: 'corpus-upload-modal',
},
computed: {
queryVariables() {
return {
projectPath: this.projectFullPath,
};
},
isUploaded() {
return Boolean(this.uploadState?.uploadedPackageId);
},
variant() {
return this.isUploaded ? 'confirm' : 'default';
},
actionPrimaryProps() {
return {
text: __('Add'),
attributes: {
'data-testid': 'modal-confirm',
disabled: !this.isUploaded,
variant: this.variant,
},
};
},
},
methods: {
addCorpus() {
return this.$apollo
.mutate({
mutation: addCorpusMutation,
variables: {
name: this.$options.i18n.newCorpus,
projectPath: this.projectFullPath,
packageId: this.uploadState?.uploadedPackageId,
},
})
.then(() => {
this.$emit('corpus-added');
});
},
resetCorpus() {
this.$apollo.mutate({
mutation: resetCorpus,
variables: { projectPath: this.projectFullPath },
});
},
beginFileUpload({ name, files }) {
if (files[0].size >= MAX_FILE_SIZE) {
this.$apollo.mutate({
mutation: uploadError,
variables: { projectPath: this.projectFullPath, error: I18N.fileTooLarge },
});
} else {
this.$apollo.mutate({
mutation: uploadCorpus,
variables: { name, projectPath: this.projectFullPath, files },
});
}
},
},
};
</script>
<template>
<div>
<gl-button
v-if="canUploadCorpus"
v-gl-modal-directive="$options.modal.modalId"
data-testid="new-corpus"
variant="confirm"
>
{{ $options.i18n.newCorpus }}
</gl-button>
<gl-modal
:modal-id="$options.modal.modalId"
:title="$options.i18n.newCorpus"
size="sm"
:action-primary="actionPrimaryProps"
:action-cancel="$options.modal.actionCancel"
@primary="addCorpus"
@canceled="resetCorpus"
>
<corpus-upload-form
:states="uploadState"
@beginFileUpload="beginFileUpload"
@resetCorpus="resetCorpus"
/>
</gl-modal>
</div>
</template>
......@@ -59,7 +59,7 @@ export default {
return !this.isUploaded && !this.isUploading;
},
isUploading() {
return this.states?.uploadState?.isUploading;
return this.states?.isUploading;
},
isUploaded() {
return this.progress === 100;
......@@ -74,16 +74,16 @@ export default {
return !this.isUploaded;
},
progress() {
return this.states?.uploadState?.progress;
return this.states?.progress;
},
progressText() {
return sprintf(__('Attaching File - %{progress}'), { progress: `${this.progress}%` });
},
nameError() {
return this.states?.uploadState?.errors.name;
return this.states?.errors.name;
},
fileError() {
return this.states?.uploadState?.errors.file;
return this.states?.errors.file;
},
},
beforeDestroy() {
......
<script>
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { __, s__ } from '~/locale';
export default {
i18n: {
emptyStateButton: s__('CorpusManagement|New corpus'),
emptyStateHeader: s__('CorpusManagement|Manage your fuzz testing corpus files'),
emptyStateLink: __('Learn more'),
emptyStateText: s__(
'CorpusManagement|A corpus is used by fuzz testing to improve coverage. Corpus files can be manually created or auto-generated. %{linkStart}Learn more%{linkEnd}',
),
},
components: {
GlEmptyState,
GlLink,
GlSprintf,
},
inject: ['emptyStateSvgPath', 'corpusHelpPath'],
};
</script>
<template>
<gl-empty-state :title="$options.i18n.emptyStateHeader" :svg-path="emptyStateSvgPath">
<template #description>
<gl-sprintf :message="$options.i18n.emptyStateText">
<template #link="{ content }">
<gl-link :href="corpusHelpPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</template>
<template #actions>
<slot name="actions"></slot>
</template>
</gl-empty-state>
</template>
......@@ -25,12 +25,21 @@ export default () => {
});
const {
dataset: { projectFullPath, canUploadCorpus, canReadCorpus, canDestroyCorpus },
dataset: {
emptyStateSvgPath,
projectFullPath,
canUploadCorpus,
canReadCorpus,
canDestroyCorpus,
},
} = el;
const corpusHelpPath = helpPagePath('user/application_security/coverage_fuzzing/index');
const corpusHelpPath = helpPagePath('user/application_security/coverage_fuzzing/index', {
anchor: 'corpus-registry',
});
const provide = {
emptyStateSvgPath,
projectFullPath,
corpusHelpPath,
canUploadCorpus: parseBoolean(canUploadCorpus),
......
......@@ -3,6 +3,7 @@
- page_title s_('CorpusManagement|Fuzz testing corpus management')
.js-corpus-management{ data: {project_full_path: @project.full_path,
empty_state_svg_path: image_path('illustrations/no_commits.svg'),
can_upload_corpus: can?(current_user, :create_package, @project).to_s,
can_read_corpus: can?(current_user, :read_package, @project).to_s,
can_destroy_corpus: can?(current_user, :destroy_package, @project).to_s } }
......@@ -6,14 +6,14 @@ exports[`EE - CorpusManagement corpus management when loaded renders the correct
class="gl-my-5"
>
Fuzz testing corpus management
Fuzz testing corpus management
</h4>
<p>
Corpus files are used in coverage-guided fuzz testing as seed inputs to improve testing.
Corpus files are used in coverage-guided fuzz testing as seed inputs to improve testing.
<gl-link-stub
href="/docs/corpus-management"
>
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EE - CorpusManagement - EmptyState should render correct content 1`] = `
<gl-empty-state-stub
invertindarkmode="true"
svgpath="/illustrations/no_commits.svg"
title="Manage your fuzz testing corpus files"
>
<gl-sprintf-stub
message="A corpus is used by fuzz testing to improve coverage. Corpus files can be manually created or auto-generated. %{linkStart}Learn more%{linkEnd}"
/>
<button>
Perform action
</button>
</gl-empty-state-stub>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Corpus Upload Button component renders header 1`] = `
<div>
<gl-button-stub
buttontextclasses=""
category="primary"
data-testid="new-corpus"
icon=""
role="button"
size="medium"
tabindex="0"
variant="confirm"
>
New corpus
</gl-button-stub>
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
dismisslabel="Close"
modalclass=""
modalid="corpus-upload-modal"
size="sm"
title="New corpus"
titletag="h4"
>
<corpus-upload-form-stub
states="[object Object]"
/>
</gl-modal-stub>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Corpus Upload component renders header 1`] = `
exports[`Corpus Upload component renders total size 1`] = `
<div
class="gl-h-11 gl-bg-gray-10 gl-display-flex gl-justify-content-space-between gl-align-items-center"
>
<div
class="gl-ml-5"
>
<gl-sprintf-stub
message="Total Size: %{totalSize}"
/>
Total Size:
<span
class="gl-font-weight-bold"
data-testid="total-size"
>
400MB
</span>
</div>
<gl-button-stub
buttontextclasses=""
category="primary"
class="gl-mr-5 gl-ml-auto"
data-testid="new-corpus"
icon=""
role="button"
size="medium"
tabindex="0"
variant="confirm"
>
New corpus
</gl-button-stub>
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
dismisslabel="Close"
modalclass=""
modalid="corpus-upload-modal"
size="sm"
title="New corpus"
titletag="h4"
>
<corpus-upload-form-stub
states="[object Object]"
/>
</gl-modal-stub>
</div>
`;
import { GlModal } from '@gitlab/ui';
import CorpusUploadButton from 'ee/security_configuration/corpus_management/components/corpus_upload_button.vue';
import CorpusUploadForm from 'ee/security_configuration/corpus_management/components/corpus_upload_form.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
const TEST_PROJECT_FULL_PATH = '/namespace/project';
describe('Corpus Upload Button', () => {
let wrapper;
const findModal = () => wrapper.findComponent(GlModal);
const findCorpusUploadForm = () => wrapper.findComponent(CorpusUploadForm);
const findNewCorpusButton = () => wrapper.findByTestId('new-corpus');
const createComponentFactory = (mountFn = shallowMountExtended) => (options = {}) => {
wrapper = mountFn(CorpusUploadButton, {
mocks: {
uploadState: {
progress: 0,
},
},
provide: {
projectFullPath: TEST_PROJECT_FULL_PATH,
canUploadCorpus: true,
},
...options,
});
};
const createComponent = createComponentFactory();
afterEach(() => {
wrapper.destroy();
});
describe('component', () => {
it('renders header', () => {
createComponent();
expect(findNewCorpusButton().exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot();
});
describe('addCorpus mutation', () => {
it('gets called when the add button is clicked from the modal', async () => {
createComponent();
jest.spyOn(wrapper.vm, 'addCorpus').mockImplementation(() => {});
await wrapper.vm.$forceUpdate();
findModal().vm.$emit('primary');
expect(wrapper.vm.addCorpus).toHaveBeenCalled();
});
});
describe('resetCorpus mutation', () => {
it('gets called when the cancel button is clicked from the modal', async () => {
createComponent();
jest.spyOn(wrapper.vm, 'resetCorpus').mockImplementation(() => {});
await wrapper.vm.$forceUpdate();
findModal().vm.$emit('canceled');
expect(wrapper.vm.resetCorpus).toHaveBeenCalled();
});
it('gets called when the upload form triggers a reset', async () => {
createComponent();
jest.spyOn(wrapper.vm, 'resetCorpus').mockImplementation(() => {});
await wrapper.vm.$forceUpdate();
findCorpusUploadForm().vm.$emit('resetCorpus');
expect(wrapper.vm.resetCorpus).toHaveBeenCalled();
});
});
describe('uploadCorpus mutation', () => {
it('gets called when the upload file is clicked from the modal', async () => {
createComponent();
jest.spyOn(wrapper.vm, 'beginFileUpload').mockImplementation(() => {});
await wrapper.vm.$forceUpdate();
findCorpusUploadForm().vm.$emit('beginFileUpload');
expect(wrapper.vm.beginFileUpload).toHaveBeenCalled();
});
});
describe('with new uploading disabled', () => {
it('does not render the upload button', () => {
createComponent({
provide: {
projectFullPath: TEST_PROJECT_FULL_PATH,
canUploadCorpus: false,
},
});
expect(findNewCorpusButton().exists()).toBe(false);
});
});
describe('add button', () => {
it('is disabled when corpus has not been uploaded', () => {
createComponent({
mocks: {
uploadState: {
progress: 0,
uploadedPackageId: null,
},
},
});
expect(findModal().props('actionPrimary')).toEqual({
attributes: {
'data-testid': 'modal-confirm',
disabled: true,
variant: 'default',
},
text: 'Add',
});
});
it('is disabled when corpus has 100 percent completion, but is still waiting on the server response', () => {
createComponent({
mocks: {
uploadState: {
progress: 100,
uploadedPackageId: null,
},
},
});
expect(findModal().props('actionPrimary')).toEqual({
attributes: {
'data-testid': 'modal-confirm',
disabled: true,
variant: 'default',
},
text: 'Add',
});
});
it('is enabled when corpus has been uploaded', () => {
createComponent({
mocks: {
uploadState: {
progress: 100,
uploadedPackageId: 1,
},
},
});
expect(findModal().props('actionPrimary')).toEqual({
attributes: {
'data-testid': 'modal-confirm',
disabled: false,
variant: 'confirm',
},
text: 'Add',
});
});
});
});
});
......@@ -44,13 +44,11 @@ describe('Corpus upload modal', () => {
const props = {
states: {
uploadState: {
isUploading: false,
progress: 0,
errors: {
name: '',
file: '',
},
isUploading: false,
progress: 0,
errors: {
name: '',
file: '',
},
},
};
......@@ -99,13 +97,11 @@ describe('Corpus upload modal', () => {
const props = {
states: {
uploadState: {
isUploading: false,
progress: 0,
errors: {
name: '',
file: '',
},
isUploading: false,
progress: 0,
errors: {
name: '',
file: '',
},
},
};
......@@ -154,13 +150,11 @@ describe('Corpus upload modal', () => {
const props = {
states: {
uploadState: {
isUploading: true,
progress: 25,
errors: {
name: '',
file: '',
},
isUploading: true,
progress: 25,
errors: {
name: '',
file: '',
},
},
};
......@@ -209,13 +203,11 @@ describe('Corpus upload modal', () => {
const props = {
states: {
uploadState: {
isUploading: false,
progress: 100,
errors: {
name: '',
file: '',
},
isUploading: false,
progress: 100,
errors: {
name: '',
file: '',
},
},
};
......@@ -256,13 +248,11 @@ describe('Corpus upload modal', () => {
const props = {
states: {
uploadState: {
isUploading: false,
progress: 0,
errors: {
name: I18N.invalidName,
file: '',
},
isUploading: false,
progress: 0,
errors: {
name: I18N.invalidName,
file: '',
},
},
};
......@@ -306,13 +296,11 @@ describe('Corpus upload modal', () => {
const props = {
states: {
uploadState: {
isUploading: false,
progress: 0,
errors: {
name: '',
file: I18N.fileTooLarge,
},
isUploading: false,
progress: 0,
errors: {
name: '',
file: I18N.fileTooLarge,
},
},
};
......@@ -356,13 +344,11 @@ describe('Corpus upload modal', () => {
const props = {
states: {
uploadState: {
isUploading: false,
progress: 0,
errors: {
name: '',
file: '',
},
isUploading: false,
progress: 0,
errors: {
name: '',
file: '',
},
},
};
......
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import CorpusUpload from 'ee/security_configuration/corpus_management/components/corpus_upload.vue';
import CorpusUploadForm from 'ee/security_configuration/corpus_management/components/corpus_upload_form.vue';
const TEST_PROJECT_FULL_PATH = '/namespace/project';
import { decimalBytes } from '~/lib/utils/unit_format';
describe('Corpus Upload', () => {
let wrapper;
const findModal = () => wrapper.findComponent(GlModal);
const findCorpusUploadForm = () => wrapper.findComponent(CorpusUploadForm);
const findNewCorpusButton = () => wrapper.find('[data-testid="new-corpus"]');
const findGlSprintf = () => wrapper.findComponent(GlSprintf);
const findTotalSizeText = () => wrapper.find('[data-testid="total-size"]');
const defaultProps = { totalSize: 4e8 };
const createComponentFactory = (mountFn = shallowMount) => (options = {}) => {
const defaultProps = { totalSize: 4e8 };
const createComponentFactory = (mountFn = mount) => (options = {}) => {
wrapper = mountFn(CorpusUpload, {
propsData: defaultProps,
mocks: {
states: {
uploadState: {
progress: 0,
},
},
},
provide: {
projectFullPath: TEST_PROJECT_FULL_PATH,
canUploadCorpus: true,
},
...options,
});
};
......@@ -38,129 +24,12 @@ describe('Corpus Upload', () => {
});
describe('component', () => {
it('renders header', () => {
it('renders total size', () => {
createComponent();
expect(findNewCorpusButton().exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot();
});
describe('addCorpus mutation', () => {
it('gets called when the add button is clicked from the modal', async () => {
createComponent();
jest.spyOn(wrapper.vm, 'addCorpus').mockImplementation(() => {});
await wrapper.vm.$forceUpdate();
findModal().vm.$emit('primary');
expect(wrapper.vm.addCorpus).toHaveBeenCalled();
});
});
describe('resetCorpus mutation', () => {
it('gets called when the cancel button is clicked from the modal', async () => {
createComponent();
jest.spyOn(wrapper.vm, 'resetCorpus').mockImplementation(() => {});
await wrapper.vm.$forceUpdate();
findModal().vm.$emit('canceled');
expect(wrapper.vm.resetCorpus).toHaveBeenCalled();
});
it('gets called when the upload form triggers a reset', async () => {
createComponent();
jest.spyOn(wrapper.vm, 'resetCorpus').mockImplementation(() => {});
await wrapper.vm.$forceUpdate();
findCorpusUploadForm().vm.$emit('resetCorpus');
expect(wrapper.vm.resetCorpus).toHaveBeenCalled();
});
});
describe('uploadCorpus mutation', () => {
it('gets called when the upload file is clicked from the modal', async () => {
createComponent();
jest.spyOn(wrapper.vm, 'beginFileUpload').mockImplementation(() => {});
await wrapper.vm.$forceUpdate();
findCorpusUploadForm().vm.$emit('beginFileUpload');
expect(wrapper.vm.beginFileUpload).toHaveBeenCalled();
});
});
describe('with new uploading disabled', () => {
it('does not render the upload button', () => {
createComponent({
provide: {
projectFullPath: TEST_PROJECT_FULL_PATH,
canUploadCorpus: false,
},
});
expect(findNewCorpusButton().exists()).toBe(false);
});
});
describe('add button', () => {
it('is disabled when corpus has not been uploaded', () => {
createComponent({
mocks: {
states: {
uploadState: {
progress: 0,
uploadedPackageId: null,
},
},
},
});
expect(findModal().props('actionPrimary')).toEqual({
attributes: {
'data-testid': 'modal-confirm',
disabled: true,
variant: 'default',
},
text: 'Add',
});
});
it('is disabled when corpus has 100 percent completion, but is still waiting on the server response', () => {
createComponent({
mocks: {
states: {
uploadState: {
progress: 100,
uploadedPackageId: null,
},
},
},
});
expect(findModal().props('actionPrimary')).toEqual({
attributes: {
'data-testid': 'modal-confirm',
disabled: true,
variant: 'default',
},
text: 'Add',
});
});
it('is enabled when corpus has been uploaded', () => {
createComponent({
mocks: {
states: {
uploadState: {
progress: 100,
uploadedPackageId: 1,
},
},
},
});
expect(findModal().props('actionPrimary')).toEqual({
attributes: {
'data-testid': 'modal-confirm',
disabled: false,
variant: 'confirm',
},
text: 'Add',
});
});
expect(findGlSprintf().exists()).toBe(true);
expect(findTotalSizeText().text()).toContain(decimalBytes(defaultProps.totalSize));
expect(wrapper.element).toMatchSnapshot();
});
});
});
......@@ -7,6 +7,7 @@ import { shallowMount } from '@vue/test-utils';
import CorpusManagement from 'ee/security_configuration/corpus_management/components/corpus_management.vue';
import CorpusTable from 'ee/security_configuration/corpus_management/components/corpus_table.vue';
import CorpusUpload from 'ee/security_configuration/corpus_management/components/corpus_upload.vue';
import EmptyState from 'ee/security_configuration/corpus_management/components/empty_state.vue';
import getCorpusesQuery from 'ee/security_configuration/corpus_management/graphql/queries/get_corpuses.query.graphql';
import deleteCorpusMutation from 'ee/security_configuration/corpus_management/graphql/mutations/delete_corpus.mutation.graphql';
......@@ -17,6 +18,7 @@ import { getCorpusesQueryResponse, deleteCorpusMutationResponse } from './mock_d
const TEST_PROJECT_FULL_PATH = '/namespace/project';
const TEST_CORPUS_HELP_PATH = '/docs/corpus-management';
const TEST_EMPTY_STATE_SVG_PATH = '/illustrations/no_commits.svg';
describe('EE - CorpusManagement', () => {
let wrapper;
......@@ -71,6 +73,7 @@ describe('EE - CorpusManagement', () => {
provide: {
projectFullPath: TEST_PROJECT_FULL_PATH,
corpusHelpPath: TEST_CORPUS_HELP_PATH,
emptyStateSvgPath: TEST_EMPTY_STATE_SVG_PATH,
},
apolloProvider: createMockApolloProvider(),
...options,
......@@ -233,10 +236,30 @@ describe('EE - CorpusManagement', () => {
createComponent();
expect(wrapper.findComponent(CorpusManagement).exists()).toBe(true);
expect(wrapper.findComponent(CorpusUpload).exists()).toBe(true);
expect(wrapper.findComponent(CorpusUpload).exists()).toBe(false);
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.findComponent(CorpusTable).exists()).toBe(false);
});
});
describe('empty state', () => {
it('should render empty state if no corpuses exist', async () => {
createComponent({
apolloProvider: createMockApolloProvider({
getCorpusesQueryRequestHandler: jest.fn().mockResolvedValue({
data: {
project: {
corpuses: {
nodes: [],
},
},
},
}),
}),
});
await waitForPromises();
expect(wrapper.findComponent(EmptyState).exists()).toBe(true);
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import EmptyState from 'ee/security_configuration/corpus_management/components/empty_state.vue';
const TEST_CORPUS_HELP_PATH = '/docs/corpus-management';
const TEST_EMPTY_STATE_SVG_PATH = '/illustrations/no_commits.svg';
describe('EE - CorpusManagement - EmptyState', () => {
let wrapper;
const testButton = '<button>Perform action</button>';
const createComponent = (options = {}) => {
wrapper = shallowMount(EmptyState, {
provide: {
corpusHelpPath: TEST_CORPUS_HELP_PATH,
emptyStateSvgPath: TEST_EMPTY_STATE_SVG_PATH,
},
slots: {
actions: testButton,
},
...options,
});
};
it('should render correct content', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
});
......@@ -10138,6 +10138,9 @@ msgstr ""
msgid "Corpus Management|Are you sure you want to delete the corpus?"
msgstr ""
msgid "CorpusManagement|A corpus is used by fuzz testing to improve coverage. Corpus files can be manually created or auto-generated. %{linkStart}Learn more%{linkEnd}"
msgstr ""
msgid "CorpusManagement|Actions"
msgstr ""
......@@ -10174,6 +10177,12 @@ msgstr ""
msgid "CorpusManagement|Latest Job:"
msgstr ""
msgid "CorpusManagement|Manage your fuzz testing corpus files"
msgstr ""
msgid "CorpusManagement|New corpus"
msgstr ""
msgid "CorpusManagement|New upload"
msgstr ""
......@@ -10189,9 +10198,6 @@ msgstr ""
msgid "CorpusManagement|Total Size: %{totalSize}"
msgstr ""
msgid "CorpusMnagement|New corpus"
msgstr ""
msgid "Could not add admins as members"
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