Commit 0229307b authored by Phil Hughes's avatar Phil Hughes

Merge branch '347272-corpus-management-empty-state' into 'master'

Corpus Management - v2 Empty state

See merge request gitlab-org/gitlab!82707
parents 19acd71b b087a911
<script> <script>
import { GlLoadingIcon, GlLink, GlKeysetPagination } from '@gitlab/ui'; import { GlLoadingIcon, GlLink, GlKeysetPagination } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql'; 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 CorpusTable from 'ee/security_configuration/corpus_management/components/corpus_table.vue';
import CorpusUpload from 'ee/security_configuration/corpus_management/components/corpus_upload.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 { s__, __ } from '~/locale';
import getCorpusesQuery from '../graphql/queries/get_corpuses.query.graphql'; import getCorpusesQuery from '../graphql/queries/get_corpuses.query.graphql';
import deleteCorpusMutation from '../graphql/mutations/delete_corpus.mutation.graphql'; import deleteCorpusMutation from '../graphql/mutations/delete_corpus.mutation.graphql';
export default { export default {
components: { components: {
EmptyState,
GlLoadingIcon, GlLoadingIcon,
GlLink, GlLink,
GlKeysetPagination, GlKeysetPagination,
CorpusTable, CorpusTable,
CorpusUpload, CorpusUpload,
CorpusUploadButton,
}, },
apollo: { apollo: {
states: { states: {
...@@ -33,7 +37,7 @@ export default { ...@@ -33,7 +37,7 @@ export default {
}, },
}, },
}, },
inject: ['projectFullPath', 'corpusHelpPath'], inject: ['emptyStateSvgPath', 'projectFullPath', 'corpusHelpPath'],
data() { data() {
return { return {
pagination: { pagination: {
...@@ -56,6 +60,9 @@ export default { ...@@ -56,6 +60,9 @@ export default {
corpuses() { corpuses() {
return this.states?.project.corpuses.nodes || []; return this.states?.project.corpuses.nodes || [];
}, },
hasCorpuses() {
return this.corpuses.length > 0;
},
pageInfo() { pageInfo() {
return this.states?.pageInfo || {}; return this.states?.pageInfo || {};
}, },
...@@ -79,8 +86,11 @@ export default { ...@@ -79,8 +86,11 @@ export default {
beforeCursor: null, beforeCursor: null,
firstPageSize: this.$options.pageSize, 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) { onDelete(id) {
return this.$apollo return this.$apollo
...@@ -116,31 +126,45 @@ export default { ...@@ -116,31 +126,45 @@ export default {
<template> <template>
<div> <div>
<header> <template v-if="!hasCorpuses">
<h4 class="gl-my-5"> <gl-loading-icon v-if="isLoading" size="lg" class="gl-py-13" />
{{ this.$options.i18n.header }} <empty-state v-else>
</h4> <template #actions>
<p> <corpus-upload-button @corpus-added="fetchCorpuses" />
{{ this.$options.i18n.subHeader }} </template>
<gl-link :href="corpusHelpPath">{{ this.$options.i18n.learnMore }}</gl-link> </empty-state>
</p> </template>
</header> <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" /> <gl-loading-icon v-if="isLoading" size="lg" class="gl-py-6" />
<template v-else> <template v-else>
<corpus-table :corpuses="corpuses" @delete="onDelete" /> <corpus-table :corpuses="corpuses" @delete="onDelete" />
</template> </template>
<div v-if="hasPagination" class="gl-display-flex gl-justify-content-center gl-mt-5"> <div v-if="hasPagination" class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-keyset-pagination <gl-keyset-pagination
v-bind="pageInfo" v-bind="pageInfo"
:prev-text="$options.i18n.previousPage" :prev-text="$options.i18n.previousPage"
:next-text="$options.i18n.nextPage" :next-text="$options.i18n.nextPage"
@prev="prevPage" @prev="prevPage"
@next="nextPage" @next="nextPage"
/> />
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { GlButton, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui'; import { GlSprintf } from '@gitlab/ui';
import { decimalBytes } from '~/lib/utils/unit_format'; import { decimalBytes } from '~/lib/utils/unit_format';
import { s__, __ } from '~/locale'; 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 { export default {
components: { components: {
GlSprintf, GlSprintf,
GlButton,
GlModal,
CorpusUploadForm,
},
directives: {
GlModalDirective,
}, },
i18n: { i18n: {
totalSize: s__('CorpusManagement|Total Size: %{totalSize}'), 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: { props: {
totalSize: { totalSize: {
...@@ -48,65 +18,9 @@ export default { ...@@ -48,65 +18,9 @@ export default {
}, },
}, },
computed: { computed: {
queryVariables() {
return {
projectPath: this.projectFullPath,
};
},
formattedFileSize() { formattedFileSize() {
return decimalBytes(this.totalSize); 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> </script>
...@@ -117,35 +31,10 @@ export default { ...@@ -117,35 +31,10 @@ export default {
<div v-if="totalSize" class="gl-ml-5"> <div v-if="totalSize" class="gl-ml-5">
<gl-sprintf :message="$options.i18n.totalSize"> <gl-sprintf :message="$options.i18n.totalSize">
<template #totalSize> <template #totalSize>
<span class="gl-font-weight-bold">{{ formattedFileSize }}</span> <span data-testid="total-size" class="gl-font-weight-bold">{{ formattedFileSize }}</span>
</template> </template>
</gl-sprintf> </gl-sprintf>
</div> </div>
<slot name="action"></slot>
<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>
</div> </div>
</template> </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 { ...@@ -59,7 +59,7 @@ export default {
return !this.isUploaded && !this.isUploading; return !this.isUploaded && !this.isUploading;
}, },
isUploading() { isUploading() {
return this.states?.uploadState?.isUploading; return this.states?.isUploading;
}, },
isUploaded() { isUploaded() {
return this.progress === 100; return this.progress === 100;
...@@ -74,16 +74,16 @@ export default { ...@@ -74,16 +74,16 @@ export default {
return !this.isUploaded; return !this.isUploaded;
}, },
progress() { progress() {
return this.states?.uploadState?.progress; return this.states?.progress;
}, },
progressText() { progressText() {
return sprintf(__('Attaching File - %{progress}'), { progress: `${this.progress}%` }); return sprintf(__('Attaching File - %{progress}'), { progress: `${this.progress}%` });
}, },
nameError() { nameError() {
return this.states?.uploadState?.errors.name; return this.states?.errors.name;
}, },
fileError() { fileError() {
return this.states?.uploadState?.errors.file; return this.states?.errors.file;
}, },
}, },
beforeDestroy() { 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 () => { ...@@ -25,12 +25,21 @@ export default () => {
}); });
const { const {
dataset: { projectFullPath, canUploadCorpus, canReadCorpus, canDestroyCorpus }, dataset: {
emptyStateSvgPath,
projectFullPath,
canUploadCorpus,
canReadCorpus,
canDestroyCorpus,
},
} = el; } = el;
const corpusHelpPath = helpPagePath('user/application_security/coverage_fuzzing/index'); const corpusHelpPath = helpPagePath('user/application_security/coverage_fuzzing/index', {
anchor: 'corpus-registry',
});
const provide = { const provide = {
emptyStateSvgPath,
projectFullPath, projectFullPath,
corpusHelpPath, corpusHelpPath,
canUploadCorpus: parseBoolean(canUploadCorpus), canUploadCorpus: parseBoolean(canUploadCorpus),
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
- page_title s_('CorpusManagement|Fuzz testing corpus management') - page_title s_('CorpusManagement|Fuzz testing corpus management')
.js-corpus-management{ data: {project_full_path: @project.full_path, .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_upload_corpus: can?(current_user, :create_package, @project).to_s,
can_read_corpus: can?(current_user, :read_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 } } 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 ...@@ -6,14 +6,14 @@ exports[`EE - CorpusManagement corpus management when loaded renders the correct
class="gl-my-5" class="gl-my-5"
> >
Fuzz testing corpus management Fuzz testing corpus management
</h4> </h4>
<p> <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 <gl-link-stub
href="/docs/corpus-management" 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 // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Corpus Upload component renders header 1`] = ` exports[`Corpus Upload component renders total size 1`] = `
<div <div
class="gl-h-11 gl-bg-gray-10 gl-display-flex gl-justify-content-space-between gl-align-items-center" class="gl-h-11 gl-bg-gray-10 gl-display-flex gl-justify-content-space-between gl-align-items-center"
> >
<div <div
class="gl-ml-5" class="gl-ml-5"
> >
<gl-sprintf-stub Total Size:
message="Total Size: %{totalSize}" <span
/> class="gl-font-weight-bold"
data-testid="total-size"
>
400MB
</span>
</div> </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> </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', () => { ...@@ -44,13 +44,11 @@ describe('Corpus upload modal', () => {
const props = { const props = {
states: { states: {
uploadState: { isUploading: false,
isUploading: false, progress: 0,
progress: 0, errors: {
errors: { name: '',
name: '', file: '',
file: '',
},
}, },
}, },
}; };
...@@ -99,13 +97,11 @@ describe('Corpus upload modal', () => { ...@@ -99,13 +97,11 @@ describe('Corpus upload modal', () => {
const props = { const props = {
states: { states: {
uploadState: { isUploading: false,
isUploading: false, progress: 0,
progress: 0, errors: {
errors: { name: '',
name: '', file: '',
file: '',
},
}, },
}, },
}; };
...@@ -154,13 +150,11 @@ describe('Corpus upload modal', () => { ...@@ -154,13 +150,11 @@ describe('Corpus upload modal', () => {
const props = { const props = {
states: { states: {
uploadState: { isUploading: true,
isUploading: true, progress: 25,
progress: 25, errors: {
errors: { name: '',
name: '', file: '',
file: '',
},
}, },
}, },
}; };
...@@ -209,13 +203,11 @@ describe('Corpus upload modal', () => { ...@@ -209,13 +203,11 @@ describe('Corpus upload modal', () => {
const props = { const props = {
states: { states: {
uploadState: { isUploading: false,
isUploading: false, progress: 100,
progress: 100, errors: {
errors: { name: '',
name: '', file: '',
file: '',
},
}, },
}, },
}; };
...@@ -256,13 +248,11 @@ describe('Corpus upload modal', () => { ...@@ -256,13 +248,11 @@ describe('Corpus upload modal', () => {
const props = { const props = {
states: { states: {
uploadState: { isUploading: false,
isUploading: false, progress: 0,
progress: 0, errors: {
errors: { name: I18N.invalidName,
name: I18N.invalidName, file: '',
file: '',
},
}, },
}, },
}; };
...@@ -306,13 +296,11 @@ describe('Corpus upload modal', () => { ...@@ -306,13 +296,11 @@ describe('Corpus upload modal', () => {
const props = { const props = {
states: { states: {
uploadState: { isUploading: false,
isUploading: false, progress: 0,
progress: 0, errors: {
errors: { name: '',
name: '', file: I18N.fileTooLarge,
file: I18N.fileTooLarge,
},
}, },
}, },
}; };
...@@ -356,13 +344,11 @@ describe('Corpus upload modal', () => { ...@@ -356,13 +344,11 @@ describe('Corpus upload modal', () => {
const props = { const props = {
states: { states: {
uploadState: { isUploading: false,
isUploading: false, progress: 0,
progress: 0, errors: {
errors: { name: '',
name: '', file: '',
file: '',
},
}, },
}, },
}; };
......
import { GlModal } from '@gitlab/ui'; import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import CorpusUpload from 'ee/security_configuration/corpus_management/components/corpus_upload.vue'; 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'; import { decimalBytes } from '~/lib/utils/unit_format';
const TEST_PROJECT_FULL_PATH = '/namespace/project';
describe('Corpus Upload', () => { describe('Corpus Upload', () => {
let wrapper; let wrapper;
const findModal = () => wrapper.findComponent(GlModal); const findGlSprintf = () => wrapper.findComponent(GlSprintf);
const findCorpusUploadForm = () => wrapper.findComponent(CorpusUploadForm); const findTotalSizeText = () => wrapper.find('[data-testid="total-size"]');
const findNewCorpusButton = () => wrapper.find('[data-testid="new-corpus"]'); const defaultProps = { totalSize: 4e8 };
const createComponentFactory = (mountFn = shallowMount) => (options = {}) => { const createComponentFactory = (mountFn = mount) => (options = {}) => {
const defaultProps = { totalSize: 4e8 };
wrapper = mountFn(CorpusUpload, { wrapper = mountFn(CorpusUpload, {
propsData: defaultProps, propsData: defaultProps,
mocks: {
states: {
uploadState: {
progress: 0,
},
},
},
provide: {
projectFullPath: TEST_PROJECT_FULL_PATH,
canUploadCorpus: true,
},
...options, ...options,
}); });
}; };
...@@ -38,129 +24,12 @@ describe('Corpus Upload', () => { ...@@ -38,129 +24,12 @@ describe('Corpus Upload', () => {
}); });
describe('component', () => { describe('component', () => {
it('renders header', () => { it('renders total size', () => {
createComponent(); 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', () => { expect(findGlSprintf().exists()).toBe(true);
it('does not render the upload button', () => { expect(findTotalSizeText().text()).toContain(decimalBytes(defaultProps.totalSize));
createComponent({ expect(wrapper.element).toMatchSnapshot();
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',
});
});
}); });
}); });
}); });
...@@ -7,6 +7,7 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -7,6 +7,7 @@ import { shallowMount } from '@vue/test-utils';
import CorpusManagement from 'ee/security_configuration/corpus_management/components/corpus_management.vue'; 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 CorpusTable from 'ee/security_configuration/corpus_management/components/corpus_table.vue';
import CorpusUpload from 'ee/security_configuration/corpus_management/components/corpus_upload.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 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'; import deleteCorpusMutation from 'ee/security_configuration/corpus_management/graphql/mutations/delete_corpus.mutation.graphql';
...@@ -17,6 +18,7 @@ import { getCorpusesQueryResponse, deleteCorpusMutationResponse } from './mock_d ...@@ -17,6 +18,7 @@ import { getCorpusesQueryResponse, deleteCorpusMutationResponse } from './mock_d
const TEST_PROJECT_FULL_PATH = '/namespace/project'; const TEST_PROJECT_FULL_PATH = '/namespace/project';
const TEST_CORPUS_HELP_PATH = '/docs/corpus-management'; const TEST_CORPUS_HELP_PATH = '/docs/corpus-management';
const TEST_EMPTY_STATE_SVG_PATH = '/illustrations/no_commits.svg';
describe('EE - CorpusManagement', () => { describe('EE - CorpusManagement', () => {
let wrapper; let wrapper;
...@@ -71,6 +73,7 @@ describe('EE - CorpusManagement', () => { ...@@ -71,6 +73,7 @@ describe('EE - CorpusManagement', () => {
provide: { provide: {
projectFullPath: TEST_PROJECT_FULL_PATH, projectFullPath: TEST_PROJECT_FULL_PATH,
corpusHelpPath: TEST_CORPUS_HELP_PATH, corpusHelpPath: TEST_CORPUS_HELP_PATH,
emptyStateSvgPath: TEST_EMPTY_STATE_SVG_PATH,
}, },
apolloProvider: createMockApolloProvider(), apolloProvider: createMockApolloProvider(),
...options, ...options,
...@@ -233,10 +236,30 @@ describe('EE - CorpusManagement', () => { ...@@ -233,10 +236,30 @@ describe('EE - CorpusManagement', () => {
createComponent(); createComponent();
expect(wrapper.findComponent(CorpusManagement).exists()).toBe(true); 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(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.findComponent(CorpusTable).exists()).toBe(false); 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 "" ...@@ -10138,6 +10138,9 @@ msgstr ""
msgid "Corpus Management|Are you sure you want to delete the corpus?" msgid "Corpus Management|Are you sure you want to delete the corpus?"
msgstr "" 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" msgid "CorpusManagement|Actions"
msgstr "" msgstr ""
...@@ -10174,6 +10177,12 @@ msgstr "" ...@@ -10174,6 +10177,12 @@ msgstr ""
msgid "CorpusManagement|Latest Job:" msgid "CorpusManagement|Latest Job:"
msgstr "" msgstr ""
msgid "CorpusManagement|Manage your fuzz testing corpus files"
msgstr ""
msgid "CorpusManagement|New corpus"
msgstr ""
msgid "CorpusManagement|New upload" msgid "CorpusManagement|New upload"
msgstr "" msgstr ""
...@@ -10189,9 +10198,6 @@ msgstr "" ...@@ -10189,9 +10198,6 @@ msgstr ""
msgid "CorpusManagement|Total Size: %{totalSize}" msgid "CorpusManagement|Total Size: %{totalSize}"
msgstr "" msgstr ""
msgid "CorpusMnagement|New corpus"
msgstr ""
msgid "Could not add admins as members" msgid "Could not add admins as members"
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