Commit 58cfa914 authored by Robert Hunt's avatar Robert Hunt Committed by Nicolò Maria Mezzopera

Refactored the compliance framework forms

- Created a constants file to hold common values
- Created new FormStatus component to separate the status of the form
and the form inputs themselves
- Updated the create form to use the FormStatus component
- Updated the edit form to use the FormStatus component
- Updated the create and edit forms to sync form data and store the
form data in the parent
- Updated the SharedForm to emit value changes rather than storing it
- Updated tests
parent 620bf1c9
<script> <script>
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
import createComplianceFrameworkMutation from '../graphql/queries/create_compliance_framework.mutation.graphql'; import createComplianceFrameworkMutation from '../graphql/queries/create_compliance_framework.mutation.graphql';
import SharedForm from './shared_form.vue'; import SharedForm from './shared_form.vue';
import FormStatus from './form_status.vue';
import { initialiseFormData, SAVE_ERROR } from '../constants';
export default { export default {
components: { components: {
FormStatus,
SharedForm, SharedForm,
}, },
props: { props: {
...@@ -22,11 +24,13 @@ export default { ...@@ -22,11 +24,13 @@ export default {
data() { data() {
return { return {
errorMessage: '', errorMessage: '',
formData: initialiseFormData(),
saving: false,
}; };
}, },
computed: { computed: {
isLoading() { isLoading() {
return this.$apollo.loading; return this.$apollo.loading || this.saving;
}, },
}, },
methods: { methods: {
...@@ -34,17 +38,21 @@ export default { ...@@ -34,17 +38,21 @@ export default {
this.errorMessage = userFriendlyText; this.errorMessage = userFriendlyText;
Sentry.captureException(error); Sentry.captureException(error);
}, },
async onSubmit(formData) { async onSubmit() {
this.saving = true;
this.errorMessage = '';
try { try {
const { name, description, color } = this.formData;
const { data } = await this.$apollo.mutate({ const { data } = await this.$apollo.mutate({
mutation: createComplianceFrameworkMutation, mutation: createComplianceFrameworkMutation,
variables: { variables: {
input: { input: {
namespacePath: this.groupPath, namespacePath: this.groupPath,
params: { params: {
name: formData.name, name,
description: formData.description, description,
color: formData.color, color,
}, },
}, },
}, },
...@@ -55,26 +63,26 @@ export default { ...@@ -55,26 +63,26 @@ export default {
if (error) { if (error) {
this.setError(new Error(error), error); this.setError(new Error(error), error);
} else { } else {
this.saving = false;
visitUrl(this.groupEditPath); visitUrl(this.groupEditPath);
} }
} catch (e) { } catch (e) {
this.setError(e, this.$options.i18n.saveError); this.setError(e, SAVE_ERROR);
} }
this.saving = false;
}, },
}, },
i18n: {
saveError: s__(
'ComplianceFrameworks|Unable to save this compliance framework. Please try again',
),
},
}; };
</script> </script>
<template> <template>
<form-status :loading="isLoading" :error="errorMessage">
<shared-form <shared-form
:group-edit-path="groupEditPath" :group-edit-path="groupEditPath"
:loading="isLoading" :name.sync="formData.name"
:render-form="!isLoading" :description.sync="formData.description"
:error="errorMessage" :color.sync="formData.color"
@submit="onSubmit" @submit="onSubmit"
/> />
</form-status>
</template> </template>
<script> <script>
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
import { convertToGraphQLId } from '~/graphql_shared/utils'; import { convertToGraphQLId } from '~/graphql_shared/utils';
import getComplianceFrameworkQuery from '../graphql/queries/get_compliance_framework.query.graphql'; import getComplianceFrameworkQuery from '../graphql/queries/get_compliance_framework.query.graphql';
import updateComplianceFrameworkMutation from '../graphql/queries/update_compliance_framework.mutation.graphql'; import updateComplianceFrameworkMutation from '../graphql/queries/update_compliance_framework.mutation.graphql';
import SharedForm from './shared_form.vue'; import SharedForm from './shared_form.vue';
import FormStatus from './form_status.vue';
import { initialiseFormData, FETCH_ERROR, SAVE_ERROR } from '../constants';
export default { export default {
components: { components: {
FormStatus,
SharedForm, SharedForm,
}, },
props: { props: {
...@@ -33,66 +35,76 @@ export default { ...@@ -33,66 +35,76 @@ export default {
}, },
data() { data() {
return { return {
complianceFramework: {},
errorMessage: '', errorMessage: '',
formData: initialiseFormData(),
saving: false,
}; };
}, },
apollo: { apollo: {
complianceFramework: { namespace: {
query: getComplianceFrameworkQuery, query: getComplianceFrameworkQuery,
variables() { variables() {
return { return {
fullPath: this.groupPath, fullPath: this.groupPath,
complianceFramework: convertToGraphQLId(this.graphqlFieldName, this.id), complianceFramework: this.graphqlId,
}; };
}, },
update(data) { result({ data }) {
this.formData = this.extractComplianceFramework(data);
},
error(error) {
this.setError(error, FETCH_ERROR);
},
},
},
computed: {
graphqlId() {
return convertToGraphQLId(this.graphqlFieldName, this.id);
},
isLoading() {
return this.$apollo.loading || this.saving;
},
hasFormData() {
return Boolean(this.formData?.name);
},
},
methods: {
extractComplianceFramework(data) {
const complianceFrameworks = data.namespace?.complianceFrameworks?.nodes || []; const complianceFrameworks = data.namespace?.complianceFrameworks?.nodes || [];
if (!complianceFrameworks.length) { if (!complianceFrameworks.length) {
this.setError(new Error(this.$options.i18n.fetchError), this.$options.i18n.fetchError); this.setError(new Error(FETCH_ERROR), FETCH_ERROR);
return {}; return initialiseFormData();
} }
const { id, name, description, color } = complianceFrameworks[0]; const { name, description, color } = complianceFrameworks[0];
return { return {
id,
name, name,
description, description,
color, color,
}; };
}, },
error(error) {
this.setError(error, this.$options.i18n.fetchError);
},
},
},
computed: {
isLoading() {
return this.$apollo.loading;
},
isFormReady() {
return Object.keys(this.complianceFramework).length > 0 && !this.isLoading;
},
},
methods: {
setError(error, userFriendlyText) { setError(error, userFriendlyText) {
this.errorMessage = userFriendlyText; this.errorMessage = userFriendlyText;
Sentry.captureException(error); Sentry.captureException(error);
}, },
async onSubmit(formData) { async onSubmit() {
this.saving = true;
this.errorMessage = '';
try { try {
const { name, description, color } = this.formData;
const { data } = await this.$apollo.mutate({ const { data } = await this.$apollo.mutate({
mutation: updateComplianceFrameworkMutation, mutation: updateComplianceFrameworkMutation,
variables: { variables: {
input: { input: {
id: this.complianceFramework.id, id: this.graphqlId,
params: { params: {
name: formData.name, name,
description: formData.description, description,
color: formData.color, color,
}, },
}, },
}, },
...@@ -103,30 +115,27 @@ export default { ...@@ -103,30 +115,27 @@ export default {
if (error) { if (error) {
this.setError(new Error(error), error); this.setError(new Error(error), error);
} else { } else {
this.saving = false;
visitUrl(this.groupEditPath); visitUrl(this.groupEditPath);
} }
} catch (e) { } catch (e) {
this.setError(e, this.$options.i18n.saveError); this.setError(e, SAVE_ERROR);
} }
this.saving = false;
}, },
}, },
i18n: {
fetchError: s__(
'ComplianceFrameworks|Error fetching compliance frameworks data. Please refresh the page',
),
saveError: s__(
'ComplianceFrameworks|Unable to save this compliance framework. Please try again',
),
},
}; };
</script> </script>
<template> <template>
<form-status :loading="isLoading" :error="errorMessage">
<shared-form <shared-form
v-if="hasFormData"
:group-edit-path="groupEditPath" :group-edit-path="groupEditPath"
:loading="isLoading" :name.sync="formData.name"
:render-form="isFormReady" :description.sync="formData.description"
:error="errorMessage" :color.sync="formData.color"
:compliance-framework="complianceFramework"
@submit="onSubmit" @submit="onSubmit"
/> />
</form-status>
</template> </template>
<script>
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
GlAlert,
GlLoadingIcon,
},
props: {
error: {
type: String,
required: false,
default: null,
},
loading: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<div class="gl-border-t-1 gl-border-t-solid gl-border-t-gray-100 gl-pt-5">
<gl-alert v-if="error" class="gl-mb-5" variant="danger" :dismissible="false">
{{ error }}
</gl-alert>
<gl-loading-icon v-if="loading" size="lg" />
<slot v-else></slot>
</div>
</template>
<script> <script>
import { import { GlButton, GlForm, GlFormGroup, GlFormInput, GlLink, GlSprintf } from '@gitlab/ui';
GlAlert,
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
GlLink,
GlLoadingIcon,
GlSprintf,
} from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { helpPagePath } from '~/helpers/help_page_helper'; import { helpPagePath } from '~/helpers/help_page_helper';
import { validateHexColor } from '~/lib/utils/color_utils'; import { validateHexColor } from '~/lib/utils/color_utils';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue'; import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue';
const hasRequiredProperties = (value) => {
if (isEmpty(value)) {
return true;
}
return ['name', 'description', 'color'].every((prop) => value[prop]);
};
export default { export default {
components: { components: {
ColorPicker, ColorPicker,
GlAlert,
GlButton, GlButton,
GlForm, GlForm,
GlFormGroup, GlFormGroup,
GlFormInput, GlFormInput,
GlLink, GlLink,
GlLoadingIcon,
GlSprintf, GlSprintf,
}, },
props: { props: {
complianceFramework: { color: {
type: Object, type: String,
required: false, required: false,
default: () => ({}), default: null,
validator: hasRequiredProperties,
}, },
error: { description: {
type: String, type: String,
required: false, required: false,
default: null, default: null,
...@@ -52,23 +31,11 @@ export default { ...@@ -52,23 +31,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
loading: { name: {
type: Boolean, type: String,
required: false,
default: false,
},
renderForm: {
type: Boolean,
required: false, required: false,
default: true, default: null,
},
}, },
data() {
return {
name: null,
description: null,
color: null,
};
}, },
computed: { computed: {
isValidColor() { isValidColor() {
...@@ -95,18 +62,6 @@ export default { ...@@ -95,18 +62,6 @@ export default {
return helpPagePath('user/project/labels.md', { anchor: 'scoped-labels' }); return helpPagePath('user/project/labels.md', { anchor: 'scoped-labels' });
}, },
}, },
watch: {
complianceFramework: {
handler() {
if (!isEmpty(this.complianceFramework)) {
this.name = this.complianceFramework.name;
this.description = this.complianceFramework.description;
this.color = this.complianceFramework.color;
}
},
immediate: true,
},
},
methods: { methods: {
onSubmit() { onSubmit() {
const { name, description, color } = this; const { name, description, color } = this;
...@@ -129,13 +84,7 @@ export default { ...@@ -129,13 +84,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="gl-border-t-1 gl-border-t-solid gl-border-t-gray-100"> <gl-form @submit.prevent="onSubmit">
<gl-alert v-if="error" class="gl-mt-5" variant="danger" :dismissible="false">
{{ error }}
</gl-alert>
<gl-loading-icon v-if="loading" size="lg" class="gl-mt-5" />
<gl-form v-if="renderForm" @submit.prevent="onSubmit">
<gl-form-group <gl-form-group
:label="$options.i18n.titleInputLabel" :label="$options.i18n.titleInputLabel"
:invalid-feedback="$options.i18n.titleInputInvalid" :invalid-feedback="$options.i18n.titleInputInvalid"
...@@ -154,7 +103,7 @@ export default { ...@@ -154,7 +103,7 @@ export default {
</gl-sprintf> </gl-sprintf>
</template> </template>
<gl-form-input :value="name" data-testid="name-input" @input="name = $event" /> <gl-form-input :value="name" data-testid="name-input" @input="$emit('update:name', $event)" />
</gl-form-group> </gl-form-group>
<gl-form-group <gl-form-group
...@@ -166,7 +115,7 @@ export default { ...@@ -166,7 +115,7 @@ export default {
<gl-form-input <gl-form-input
:value="description" :value="description"
data-testid="description-input" data-testid="description-input"
@input="description = $event" @input="$emit('update:description', $event)"
/> />
</gl-form-group> </gl-form-group>
...@@ -174,7 +123,7 @@ export default { ...@@ -174,7 +123,7 @@ export default {
:value="color" :value="color"
:label="$options.i18n.colorInputLabel" :label="$options.i18n.colorInputLabel"
:state="isValidColor" :state="isValidColor"
@input="color = $event" @input="$emit('update:color', $event)"
/> />
<div <div
...@@ -193,5 +142,4 @@ export default { ...@@ -193,5 +142,4 @@ export default {
}}</gl-button> }}</gl-button>
</div> </div>
</gl-form> </gl-form>
</div>
</template> </template>
import { s__ } from '~/locale';
export const initialiseFormData = () => ({
name: null,
description: null,
color: null,
});
export const FETCH_ERROR = s__(
'ComplianceFrameworks|Error fetching compliance frameworks data. Please refresh the page',
);
export const SAVE_ERROR = s__(
'ComplianceFrameworks|Unable to save this compliance framework. Please try again',
);
...@@ -7,6 +7,8 @@ import createMockApollo from 'helpers/mock_apollo_helper'; ...@@ -7,6 +7,8 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import createComplianceFrameworkMutation from 'ee/groups/settings/compliance_frameworks/graphql/queries/create_compliance_framework.mutation.graphql'; import createComplianceFrameworkMutation from 'ee/groups/settings/compliance_frameworks/graphql/queries/create_compliance_framework.mutation.graphql';
import CreateForm from 'ee/groups/settings/compliance_frameworks/components/create_form.vue'; import CreateForm from 'ee/groups/settings/compliance_frameworks/components/create_form.vue';
import SharedForm from 'ee/groups/settings/compliance_frameworks/components/shared_form.vue'; import SharedForm from 'ee/groups/settings/compliance_frameworks/components/shared_form.vue';
import FormStatus from 'ee/groups/settings/compliance_frameworks/components/form_status.vue';
import { SAVE_ERROR } from 'ee/groups/settings/compliance_frameworks/constants';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
...@@ -17,7 +19,7 @@ localVue.use(VueApollo); ...@@ -17,7 +19,7 @@ localVue.use(VueApollo);
jest.mock('~/lib/utils/url_utility'); jest.mock('~/lib/utils/url_utility');
describe('Form', () => { describe('CreateForm', () => {
let wrapper; let wrapper;
const sentryError = new Error('Network error'); const sentryError = new Error('Network error');
const sentrySaveError = new Error('Invalid values given'); const sentrySaveError = new Error('Invalid values given');
...@@ -32,6 +34,7 @@ describe('Form', () => { ...@@ -32,6 +34,7 @@ describe('Form', () => {
const createWithErrors = jest.fn().mockResolvedValue(errorCreateResponse); const createWithErrors = jest.fn().mockResolvedValue(errorCreateResponse);
const findForm = () => wrapper.findComponent(SharedForm); const findForm = () => wrapper.findComponent(SharedForm);
const findFormStatus = () => wrapper.findComponent(FormStatus);
function createMockApolloProvider(requestHandlers) { function createMockApolloProvider(requestHandlers) {
localVue.use(VueApollo); localVue.use(VueApollo);
...@@ -47,6 +50,17 @@ describe('Form', () => { ...@@ -47,6 +50,17 @@ describe('Form', () => {
}); });
} }
async function submitForm(name, description, color) {
await waitForPromises();
findForm().vm.$emit('update:name', name);
findForm().vm.$emit('update:description', description);
findForm().vm.$emit('update:color', color);
findForm().vm.$emit('submit');
await waitForPromises();
}
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -56,9 +70,8 @@ describe('Form', () => { ...@@ -56,9 +70,8 @@ describe('Form', () => {
wrapper = createComponent(); wrapper = createComponent();
}); });
it('passes the loading state to the form', () => { it('passes the loading state to the form status', () => {
expect(findForm().props('loading')).toBe(false); expect(findFormStatus().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
}); });
}); });
...@@ -77,50 +90,39 @@ describe('Form', () => { ...@@ -77,50 +90,39 @@ describe('Form', () => {
}, },
}; };
it('passes the error to the form when saving causes an exception and does not redirect', async () => { it('passes the error to the form status when saving causes an exception and does not redirect', async () => {
jest.spyOn(Sentry, 'captureException'); jest.spyOn(Sentry, 'captureException');
wrapper = createComponent([[createComplianceFrameworkMutation, createWithNetworkErrors]]); wrapper = createComponent([[createComplianceFrameworkMutation, createWithNetworkErrors]]);
await waitForPromises(); await submitForm(name, description, color);
findForm().vm.$emit('submit', { name, description, color });
await waitForPromises();
expect(createWithNetworkErrors).toHaveBeenCalledWith(creationProps); expect(createWithNetworkErrors).toHaveBeenCalledWith(creationProps);
expect(findForm().props('loading')).toBe(false); expect(findFormStatus().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
expect(visitUrl).not.toHaveBeenCalled(); expect(visitUrl).not.toHaveBeenCalled();
expect(findForm().props('error')).toBe( expect(findFormStatus().props('error')).toBe(SAVE_ERROR);
'Unable to save this compliance framework. Please try again',
);
expect(Sentry.captureException.mock.calls[0][0].networkError).toStrictEqual(sentryError); expect(Sentry.captureException.mock.calls[0][0].networkError).toStrictEqual(sentryError);
}); });
it('passes the errors to the form when saving fails and does not redirect', async () => { it('passes the errors to the form status when saving fails and does not redirect', async () => {
jest.spyOn(Sentry, 'captureException'); jest.spyOn(Sentry, 'captureException');
wrapper = createComponent([[createComplianceFrameworkMutation, createWithErrors]]); wrapper = createComponent([[createComplianceFrameworkMutation, createWithErrors]]);
await waitForPromises(); await submitForm(name, description, color);
findForm().vm.$emit('submit', { name, description, color });
await waitForPromises();
expect(createWithErrors).toHaveBeenCalledWith(creationProps); expect(createWithErrors).toHaveBeenCalledWith(creationProps);
expect(findForm().props('loading')).toBe(false); expect(findFormStatus().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
expect(visitUrl).not.toHaveBeenCalled(); expect(visitUrl).not.toHaveBeenCalled();
expect(findForm().props('error')).toBe('Invalid values given'); expect(findFormStatus().props('error')).toBe('Invalid values given');
expect(Sentry.captureException.mock.calls[0][0]).toStrictEqual(sentrySaveError); expect(Sentry.captureException.mock.calls[0][0]).toStrictEqual(sentrySaveError);
}); });
it('saves inputted values and redirects', async () => { it('saves inputted values and redirects', async () => {
wrapper = createComponent([[createComplianceFrameworkMutation, create]]); wrapper = createComponent([[createComplianceFrameworkMutation, create]]);
await waitForPromises(); await submitForm(name, description, color);
findForm().vm.$emit('submit', { name, description, color });
await waitForPromises();
expect(create).toHaveBeenCalledWith(creationProps); expect(create).toHaveBeenCalledWith(creationProps);
expect(findForm().props('loading')).toBe(false); expect(findFormStatus().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
expect(visitUrl).toHaveBeenCalledWith(propsData.groupEditPath); expect(visitUrl).toHaveBeenCalledWith(propsData.groupEditPath);
}); });
}); });
......
...@@ -8,6 +8,8 @@ import getComplianceFrameworkQuery from 'ee/groups/settings/compliance_framework ...@@ -8,6 +8,8 @@ import getComplianceFrameworkQuery from 'ee/groups/settings/compliance_framework
import updateComplianceFrameworkMutation from 'ee/groups/settings/compliance_frameworks/graphql/queries/update_compliance_framework.mutation.graphql'; import updateComplianceFrameworkMutation from 'ee/groups/settings/compliance_frameworks/graphql/queries/update_compliance_framework.mutation.graphql';
import EditForm from 'ee/groups/settings/compliance_frameworks/components/edit_form.vue'; import EditForm from 'ee/groups/settings/compliance_frameworks/components/edit_form.vue';
import SharedForm from 'ee/groups/settings/compliance_frameworks/components/shared_form.vue'; import SharedForm from 'ee/groups/settings/compliance_frameworks/components/shared_form.vue';
import FormStatus from 'ee/groups/settings/compliance_frameworks/components/form_status.vue';
import { FETCH_ERROR, SAVE_ERROR } from 'ee/groups/settings/compliance_frameworks/constants';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
...@@ -24,12 +26,12 @@ localVue.use(VueApollo); ...@@ -24,12 +26,12 @@ localVue.use(VueApollo);
jest.mock('~/lib/utils/url_utility'); jest.mock('~/lib/utils/url_utility');
describe('Form', () => { describe('EditForm', () => {
let wrapper; let wrapper;
const sentryError = new Error('Network error'); const sentryError = new Error('Network error');
const sentrySaveError = new Error('Invalid values given'); const sentrySaveError = new Error('Invalid values given');
const propsData = { const propsData = {
graphqlFieldName: 'field', graphqlFieldName: 'ComplianceManagement::Framework',
groupPath: 'group-1', groupPath: 'group-1',
groupEditPath: 'group-1/edit', groupEditPath: 'group-1/edit',
id: '1', id: '1',
...@@ -46,6 +48,7 @@ describe('Form', () => { ...@@ -46,6 +48,7 @@ describe('Form', () => {
const updateWithErrors = jest.fn().mockResolvedValue(errorUpdateResponse); const updateWithErrors = jest.fn().mockResolvedValue(errorUpdateResponse);
const findForm = () => wrapper.findComponent(SharedForm); const findForm = () => wrapper.findComponent(SharedForm);
const findFormStatus = () => wrapper.findComponent(FormStatus);
function createMockApolloProvider(requestHandlers) { function createMockApolloProvider(requestHandlers) {
localVue.use(VueApollo); localVue.use(VueApollo);
...@@ -61,6 +64,17 @@ describe('Form', () => { ...@@ -61,6 +64,17 @@ describe('Form', () => {
}); });
} }
async function submitForm(name, description, color) {
await waitForPromises();
findForm().vm.$emit('update:name', name);
findForm().vm.$emit('update:description', description);
findForm().vm.$emit('update:color', color);
findForm().vm.$emit('submit');
await waitForPromises();
}
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -70,9 +84,8 @@ describe('Form', () => { ...@@ -70,9 +84,8 @@ describe('Form', () => {
wrapper = createComponent([[getComplianceFrameworkQuery, fetchLoading]]); wrapper = createComponent([[getComplianceFrameworkQuery, fetchLoading]]);
}); });
it('passes the loading state to the form', () => { it('passes the loading state to the form status', () => {
expect(findForm().props('loading')).toBe(true); expect(findFormStatus().props('loading')).toBe(true);
expect(findForm().props('renderForm')).toBe(false);
}); });
}); });
...@@ -83,39 +96,37 @@ describe('Form', () => { ...@@ -83,39 +96,37 @@ describe('Form', () => {
await waitForPromises(); await waitForPromises();
expect(fetchOne).toHaveBeenCalledTimes(1); expect(fetchOne).toHaveBeenCalledTimes(1);
expect(findForm().props('complianceFramework')).toMatchObject(frameworkFoundResponse); expect(findForm().props()).toMatchObject({
expect(findForm().props('renderForm')).toBe(true); name: frameworkFoundResponse.name,
description: frameworkFoundResponse.description,
color: frameworkFoundResponse.color,
groupEditPath: propsData.groupEditPath,
});
expect(findForm().exists()).toBe(true);
}); });
it('passes the error to the form if the existing framework query returns no data', async () => { it('passes the error to the form status if the existing framework query returns no data', async () => {
jest.spyOn(Sentry, 'captureException'); jest.spyOn(Sentry, 'captureException');
wrapper = createComponent([[getComplianceFrameworkQuery, fetchEmpty]]); wrapper = createComponent([[getComplianceFrameworkQuery, fetchEmpty]]);
await waitForPromises(); await waitForPromises();
expect(fetchEmpty).toHaveBeenCalledTimes(1); expect(fetchEmpty).toHaveBeenCalledTimes(1);
expect(findForm().props('loading')).toBe(false); expect(findFormStatus().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(false); expect(findFormStatus().props('error')).toBe(FETCH_ERROR);
expect(findForm().props('error')).toBe( expect(Sentry.captureException.mock.calls[0][0]).toStrictEqual(new Error(FETCH_ERROR));
'Error fetching compliance frameworks data. Please refresh the page',
);
expect(Sentry.captureException.mock.calls[0][0]).toStrictEqual(
new Error('Error fetching compliance frameworks data. Please refresh the page'),
);
}); });
it('passes the error to the form if the existing framework query fails', async () => { it('passes the error to the form status if the existing framework query fails', async () => {
jest.spyOn(Sentry, 'captureException'); jest.spyOn(Sentry, 'captureException');
wrapper = createComponent([[getComplianceFrameworkQuery, fetchWithErrors]]); wrapper = createComponent([[getComplianceFrameworkQuery, fetchWithErrors]]);
await waitForPromises(); await waitForPromises();
expect(fetchWithErrors).toHaveBeenCalledTimes(1); expect(fetchWithErrors).toHaveBeenCalledTimes(1);
expect(findForm().props('loading')).toBe(false); expect(findFormStatus().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(false); expect(findForm().exists()).toBe(false);
expect(findForm().props('error')).toBe( expect(findFormStatus().props('error')).toBe(FETCH_ERROR);
'Error fetching compliance frameworks data. Please refresh the page',
);
expect(Sentry.captureException.mock.calls[0][0].networkError).toBe(sentryError); expect(Sentry.captureException.mock.calls[0][0].networkError).toBe(sentryError);
}); });
}); });
...@@ -135,43 +146,35 @@ describe('Form', () => { ...@@ -135,43 +146,35 @@ describe('Form', () => {
}, },
}; };
it('passes the error to the form when saving causes an exception and does not redirect', async () => { it('passes the error to the form status when saving causes an exception and does not redirect', async () => {
jest.spyOn(Sentry, 'captureException'); jest.spyOn(Sentry, 'captureException');
wrapper = createComponent([ wrapper = createComponent([
[getComplianceFrameworkQuery, fetchOne], [getComplianceFrameworkQuery, fetchOne],
[updateComplianceFrameworkMutation, updateWithNetworkErrors], [updateComplianceFrameworkMutation, updateWithNetworkErrors],
]); ]);
await waitForPromises(); await submitForm(name, description, color);
findForm().vm.$emit('submit', { name, description, color });
await waitForPromises();
expect(updateWithNetworkErrors).toHaveBeenCalledWith(updateProps); expect(updateWithNetworkErrors).toHaveBeenCalledWith(updateProps);
expect(findForm().props('loading')).toBe(false); expect(findFormStatus().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
expect(visitUrl).not.toHaveBeenCalled(); expect(visitUrl).not.toHaveBeenCalled();
expect(findForm().props('error')).toBe( expect(findFormStatus().props('error')).toBe(SAVE_ERROR);
'Unable to save this compliance framework. Please try again',
);
expect(Sentry.captureException.mock.calls[0][0].networkError).toStrictEqual(sentryError); expect(Sentry.captureException.mock.calls[0][0].networkError).toStrictEqual(sentryError);
}); });
it('passes the errors to the form when saving fails and does not redirect', async () => { it('passes the errors to the form status when saving fails and does not redirect', async () => {
jest.spyOn(Sentry, 'captureException'); jest.spyOn(Sentry, 'captureException');
wrapper = createComponent([ wrapper = createComponent([
[getComplianceFrameworkQuery, fetchOne], [getComplianceFrameworkQuery, fetchOne],
[updateComplianceFrameworkMutation, updateWithErrors], [updateComplianceFrameworkMutation, updateWithErrors],
]); ]);
await waitForPromises(); await submitForm(name, description, color);
findForm().vm.$emit('submit', { name, description, color });
await waitForPromises();
expect(updateWithErrors).toHaveBeenCalledWith(updateProps); expect(updateWithErrors).toHaveBeenCalledWith(updateProps);
expect(findForm().props('loading')).toBe(false); expect(findFormStatus().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
expect(visitUrl).not.toHaveBeenCalled(); expect(visitUrl).not.toHaveBeenCalled();
expect(findForm().props('error')).toBe('Invalid values given'); expect(findFormStatus().props('error')).toBe('Invalid values given');
expect(Sentry.captureException.mock.calls[0][0]).toStrictEqual(sentrySaveError); expect(Sentry.captureException.mock.calls[0][0]).toStrictEqual(sentrySaveError);
}); });
...@@ -181,13 +184,10 @@ describe('Form', () => { ...@@ -181,13 +184,10 @@ describe('Form', () => {
[updateComplianceFrameworkMutation, update], [updateComplianceFrameworkMutation, update],
]); ]);
await waitForPromises(); await submitForm(name, description, color);
findForm().vm.$emit('submit', { name, description, color });
await waitForPromises();
expect(update).toHaveBeenCalledWith(updateProps); expect(update).toHaveBeenCalledWith(updateProps);
expect(findForm().props('loading')).toBe(false); expect(findFormStatus().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
expect(visitUrl).toHaveBeenCalledWith(propsData.groupEditPath); expect(visitUrl).toHaveBeenCalledWith(propsData.groupEditPath);
}); });
}); });
......
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import FormStatus from 'ee/groups/settings/compliance_frameworks/components/form_status.vue';
describe('FormStatus', () => {
let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
function createComponent(props = {}) {
return shallowMount(FormStatus, {
propsData: {
...props,
},
slots: {
default: '<span data-testid="default-slot">Form</span>',
},
stubs: {
GlLoadingIcon,
},
});
}
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
describe('Error alert', () => {
it('shows the alert when an error are passed in', () => {
const error = 'Bad things happened';
wrapper = createComponent({ error });
expect(findAlert().text()).toBe(error);
expect(findDefaultSlot().exists()).toBe(true);
});
});
describe('Loading', () => {
it('shows the loading icon when loading is passed in', () => {
wrapper = createComponent({ loading: true });
expect(findLoadingIcon().exists()).toBe(true);
expect(findDefaultSlot().exists()).toBe(false);
});
});
describe('Default slot', () => {
it('shows by default', () => {
wrapper = createComponent();
expect(findDefaultSlot().exists()).toBe(true);
});
});
});
import { GlAlert, GlLoadingIcon, GlForm, GlSprintf } from '@gitlab/ui'; import { GlForm, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlFormGroup } from 'jest/registry/shared/stubs'; import { GlFormGroup } from 'jest/registry/shared/stubs';
import SharedForm from 'ee/groups/settings/compliance_frameworks/components/shared_form.vue'; import SharedForm from 'ee/groups/settings/compliance_frameworks/components/shared_form.vue';
import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue'; import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue';
import { frameworkFoundResponse } from '../mock_data'; import { frameworkFoundResponse, suggestedLabelColors } from '../mock_data';
describe('Form', () => { describe('SharedForm', () => {
let wrapper; let wrapper;
const defaultPropsData = { groupEditPath: 'group-1' }; const defaultPropsData = { groupEditPath: 'group-1' };
const findAlert = () => wrapper.findComponent(GlAlert);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findForm = () => wrapper.findComponent(GlForm); const findForm = () => wrapper.findComponent(GlForm);
const findNameGroup = () => wrapper.find('[data-testid="name-input-group"]'); const findNameGroup = () => wrapper.find('[data-testid="name-input-group"]');
const findNameInput = () => wrapper.find('[data-testid="name-input"]'); const findNameInput = () => wrapper.find('[data-testid="name-input"]');
...@@ -29,59 +27,26 @@ describe('Form', () => { ...@@ -29,59 +27,26 @@ describe('Form', () => {
...props, ...props,
}, },
stubs: { stubs: {
GlLoadingIcon,
GlFormGroup, GlFormGroup,
GlSprintf, GlSprintf,
}, },
}); });
} }
beforeAll(() => {
gon.suggested_label_colors = suggestedLabelColors;
});
afterEach(() => { afterEach(() => {
if (wrapper) { if (wrapper) {
wrapper.destroy(); wrapper.destroy();
} }
}); });
describe('Loading', () => {
it.each`
loading
${true}
${false}
`('renders the app correctly', ({ loading }) => {
wrapper = createComponent({ loading });
expect(findLoadingIcon().exists()).toBe(loading);
expect(findAlert().exists()).toBe(false);
});
});
describe('Rendering the form', () => {
it.each`
renderForm
${true}
${false}
`('renders the app correctly when the renderForm prop is passed', ({ renderForm }) => {
wrapper = createComponent({ renderForm });
expect(findLoadingIcon().exists()).toBe(false);
expect(findAlert().exists()).toBe(false);
expect(findForm().exists()).toBe(renderForm);
});
});
describe('Error alert', () => {
it('shows the alert when an error are passed in', () => {
wrapper = createComponent({ error: 'Bad things happened' });
expect(findAlert().text()).toBe('Bad things happened');
});
});
describe('Fields', () => { describe('Fields', () => {
it('shows the correct input and button fields', () => { it('shows the correct input and button fields', () => {
wrapper = createComponent(); wrapper = createComponent();
expect(findLoadingIcon().exists()).toBe(false);
expect(findNameInput()).toExist(); expect(findNameInput()).toExist();
expect(findDescriptionInput()).toExist(); expect(findDescriptionInput()).toExist();
expect(findColorPicker()).toExist(); expect(findColorPicker()).toExist();
...@@ -97,19 +62,14 @@ describe('Form', () => { ...@@ -97,19 +62,14 @@ describe('Form', () => {
}); });
describe('Validation', () => { describe('Validation', () => {
it('throws an error if the provided compliance framework is invalid', () => {
expect(SharedForm.props.complianceFramework.validator({ foo: 'bar' })).toBe(false);
});
it.each` it.each`
name | validity name | validity
${null} | ${null} ${null} | ${null}
${''} | ${false} ${''} | ${false}
${'foobar'} | ${true} ${'foobar'} | ${true}
`('sends the correct state to the name input group', async ({ name, validity }) => { `('sets the correct state to the name input group', ({ name, validity }) => {
wrapper = createComponent(); wrapper = createComponent({ name });
await findNameInput().vm.$emit('input', name);
expect(findNameGroup().props('state')).toBe(validity); expect(findNameGroup().props('state')).toBe(validity);
}); });
...@@ -118,15 +78,11 @@ describe('Form', () => { ...@@ -118,15 +78,11 @@ describe('Form', () => {
${null} | ${null} ${null} | ${null}
${''} | ${false} ${''} | ${false}
${'foobar'} | ${true} ${'foobar'} | ${true}
`( `('sets the correct state to the description input group', ({ description, validity }) => {
'sends the correct state to the description input group', wrapper = createComponent({ description });
async ({ description, validity }) => {
wrapper = createComponent();
await findDescriptionInput().vm.$emit('input', description);
expect(findDescriptionGroup().props('state')).toBe(validity); expect(findDescriptionGroup().props('state')).toBe(validity);
}, });
);
it.each` it.each`
color | validity color | validity
...@@ -136,12 +92,10 @@ describe('Form', () => { ...@@ -136,12 +92,10 @@ describe('Form', () => {
${'#00'} | ${false} ${'#00'} | ${false}
${'#000'} | ${true} ${'#000'} | ${true}
${'#000000'} | ${true} ${'#000000'} | ${true}
`('sends the correct state to the color picker', async ({ color, validity }) => { `('sets the correct state to the color picker', ({ color, validity }) => {
wrapper = createComponent(); wrapper = createComponent({ color });
const colorPicker = findColorPicker();
await colorPicker.vm.$emit('input', color); expect(findColorPicker().props('state')).toBe(validity);
expect(colorPicker.props('state')).toBe(validity);
}); });
it.each` it.each`
...@@ -154,12 +108,8 @@ describe('Form', () => { ...@@ -154,12 +108,8 @@ describe('Form', () => {
${'Foo'} | ${'Bar'} | ${'#000'} | ${undefined} ${'Foo'} | ${'Bar'} | ${'#000'} | ${undefined}
`( `(
'should set the submit buttons disabled attribute to $disabled', 'should set the submit buttons disabled attribute to $disabled',
async ({ name, description, color, disabled }) => { ({ name, description, color, disabled }) => {
wrapper = createComponent(); wrapper = createComponent({ name, description, color });
await findNameInput().vm.$emit('input', name);
await findDescriptionInput().vm.$emit('input', description);
await findColorPicker().vm.$emit('input', color);
expect(findSubmitBtn().attributes('disabled')).toBe(disabled); expect(findSubmitBtn().attributes('disabled')).toBe(disabled);
}, },
...@@ -167,50 +117,31 @@ describe('Form', () => { ...@@ -167,50 +117,31 @@ describe('Form', () => {
}); });
describe('Updating data', () => { describe('Updating data', () => {
it('updates the initial form data when the compliance framework prop is updated', async () => { it('updates the initial form data when the props are updated', async () => {
const { name, description, color } = frameworkFoundResponse;
wrapper = createComponent(); wrapper = createComponent();
expect(findNameInput().attributes('value')).toBe(undefined); expect(findNameInput().attributes('value')).toBe(undefined);
expect(findDescriptionInput().attributes('value')).toBe(undefined); expect(findDescriptionInput().attributes('value')).toBe(undefined);
expect(findColorPicker().attributes('value')).toBe(undefined); expect(findColorPicker().attributes('value')).toBe(undefined);
await wrapper.setProps({ complianceFramework: frameworkFoundResponse }); await wrapper.setProps({ name, description, color });
expect(findNameInput().attributes('value')).toBe(frameworkFoundResponse.name); expect(findNameInput().attributes('value')).toBe(name);
expect(findDescriptionInput().attributes('value')).toBe(frameworkFoundResponse.description); expect(findDescriptionInput().attributes('value')).toBe(description);
expect(findColorPicker().attributes('value')).toBe(frameworkFoundResponse.color); expect(findColorPicker().attributes('value')).toBe(color);
}); });
}); });
describe('On form submission', () => { describe('On form submission', () => {
it('emits the entered form data', async () => { it('emits a submit event', async () => {
wrapper = createComponent(); const { name, description, color } = frameworkFoundResponse;
wrapper = createComponent({ name, description, color });
await findNameInput().vm.$emit('input', 'Foo');
await findDescriptionInput().vm.$emit('input', 'Bar');
await findColorPicker().vm.$emit('input', '#000');
await findForm().vm.$emit('submit', { preventDefault: () => {} });
expect(wrapper.emitted('submit')).toHaveLength(1);
expect(wrapper.emitted('submit')[0]).toEqual([
{ name: 'Foo', description: 'Bar', color: '#000' },
]);
});
it('does not emit the initial form data if editing has taken place', async () => {
wrapper = createComponent({ complianceFramework: frameworkFoundResponse });
await findNameInput().vm.$emit('input', 'Foo');
await findDescriptionInput().vm.$emit('input', 'Bar');
await findColorPicker().vm.$emit('input', '#000');
await findForm().vm.$emit('submit', { preventDefault: () => {} }); await findForm().vm.$emit('submit', { preventDefault: () => {} });
expect(wrapper.emitted('submit')).toHaveLength(1); expect(wrapper.emitted('submit')).toHaveLength(1);
expect(wrapper.emitted('submit')[0]).toEqual([ expect(wrapper.emitted('submit')[0]).toEqual([{ name, description, color }]);
{ name: 'Foo', description: 'Bar', color: '#000' },
]);
}); });
}); });
}); });
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