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>
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