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>
import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import * as Sentry from '~/sentry/wrapper';
import createComplianceFrameworkMutation from '../graphql/queries/create_compliance_framework.mutation.graphql';
import SharedForm from './shared_form.vue';
import FormStatus from './form_status.vue';
import { initialiseFormData, SAVE_ERROR } from '../constants';
export default {
components: {
FormStatus,
SharedForm,
},
props: {
......@@ -22,11 +24,13 @@ export default {
data() {
return {
errorMessage: '',
formData: initialiseFormData(),
saving: false,
};
},
computed: {
isLoading() {
return this.$apollo.loading;
return this.$apollo.loading || this.saving;
},
},
methods: {
......@@ -34,17 +38,21 @@ export default {
this.errorMessage = userFriendlyText;
Sentry.captureException(error);
},
async onSubmit(formData) {
async onSubmit() {
this.saving = true;
this.errorMessage = '';
try {
const { name, description, color } = this.formData;
const { data } = await this.$apollo.mutate({
mutation: createComplianceFrameworkMutation,
variables: {
input: {
namespacePath: this.groupPath,
params: {
name: formData.name,
description: formData.description,
color: formData.color,
name,
description,
color,
},
},
},
......@@ -55,26 +63,26 @@ export default {
if (error) {
this.setError(new Error(error), error);
} else {
this.saving = false;
visitUrl(this.groupEditPath);
}
} 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>
<template>
<shared-form
:group-edit-path="groupEditPath"
:loading="isLoading"
:render-form="!isLoading"
:error="errorMessage"
@submit="onSubmit"
/>
<form-status :loading="isLoading" :error="errorMessage">
<shared-form
:group-edit-path="groupEditPath"
:name.sync="formData.name"
:description.sync="formData.description"
:color.sync="formData.color"
@submit="onSubmit"
/>
</form-status>
</template>
<script>
import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import * as Sentry from '~/sentry/wrapper';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import getComplianceFrameworkQuery from '../graphql/queries/get_compliance_framework.query.graphql';
import updateComplianceFrameworkMutation from '../graphql/queries/update_compliance_framework.mutation.graphql';
import SharedForm from './shared_form.vue';
import FormStatus from './form_status.vue';
import { initialiseFormData, FETCH_ERROR, SAVE_ERROR } from '../constants';
export default {
components: {
FormStatus,
SharedForm,
},
props: {
......@@ -33,66 +35,76 @@ export default {
},
data() {
return {
complianceFramework: {},
errorMessage: '',
formData: initialiseFormData(),
saving: false,
};
},
apollo: {
complianceFramework: {
namespace: {
query: getComplianceFrameworkQuery,
variables() {
return {
fullPath: this.groupPath,
complianceFramework: convertToGraphQLId(this.graphqlFieldName, this.id),
complianceFramework: this.graphqlId,
};
},
update(data) {
const complianceFrameworks = data.namespace?.complianceFrameworks?.nodes || [];
if (!complianceFrameworks.length) {
this.setError(new Error(this.$options.i18n.fetchError), this.$options.i18n.fetchError);
return {};
}
const { id, name, description, color } = complianceFrameworks[0];
return {
id,
name,
description,
color,
};
result({ data }) {
this.formData = this.extractComplianceFramework(data);
},
error(error) {
this.setError(error, this.$options.i18n.fetchError);
this.setError(error, FETCH_ERROR);
},
},
},
computed: {
graphqlId() {
return convertToGraphQLId(this.graphqlFieldName, this.id);
},
isLoading() {
return this.$apollo.loading;
return this.$apollo.loading || this.saving;
},
isFormReady() {
return Object.keys(this.complianceFramework).length > 0 && !this.isLoading;
hasFormData() {
return Boolean(this.formData?.name);
},
},
methods: {
extractComplianceFramework(data) {
const complianceFrameworks = data.namespace?.complianceFrameworks?.nodes || [];
if (!complianceFrameworks.length) {
this.setError(new Error(FETCH_ERROR), FETCH_ERROR);
return initialiseFormData();
}
const { name, description, color } = complianceFrameworks[0];
return {
name,
description,
color,
};
},
setError(error, userFriendlyText) {
this.errorMessage = userFriendlyText;
Sentry.captureException(error);
},
async onSubmit(formData) {
async onSubmit() {
this.saving = true;
this.errorMessage = '';
try {
const { name, description, color } = this.formData;
const { data } = await this.$apollo.mutate({
mutation: updateComplianceFrameworkMutation,
variables: {
input: {
id: this.complianceFramework.id,
id: this.graphqlId,
params: {
name: formData.name,
description: formData.description,
color: formData.color,
name,
description,
color,
},
},
},
......@@ -103,30 +115,27 @@ export default {
if (error) {
this.setError(new Error(error), error);
} else {
this.saving = false;
visitUrl(this.groupEditPath);
}
} 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>
<template>
<shared-form
:group-edit-path="groupEditPath"
:loading="isLoading"
:render-form="isFormReady"
:error="errorMessage"
:compliance-framework="complianceFramework"
@submit="onSubmit"
/>
<form-status :loading="isLoading" :error="errorMessage">
<shared-form
v-if="hasFormData"
:group-edit-path="groupEditPath"
:name.sync="formData.name"
:description.sync="formData.description"
:color.sync="formData.color"
@submit="onSubmit"
/>
</form-status>
</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>
import {
GlAlert,
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
GlLink,
GlLoadingIcon,
GlSprintf,
} from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { GlButton, GlForm, GlFormGroup, GlFormInput, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { validateHexColor } from '~/lib/utils/color_utils';
import { __, s__ } from '~/locale';
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 {
components: {
ColorPicker,
GlAlert,
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
GlLink,
GlLoadingIcon,
GlSprintf,
},
props: {
complianceFramework: {
type: Object,
color: {
type: String,
required: false,
default: () => ({}),
validator: hasRequiredProperties,
default: null,
},
error: {
description: {
type: String,
required: false,
default: null,
......@@ -52,24 +31,12 @@ export default {
type: String,
required: true,
},
loading: {
type: Boolean,
required: false,
default: false,
},
renderForm: {
type: Boolean,
name: {
type: String,
required: false,
default: true,
default: null,
},
},
data() {
return {
name: null,
description: null,
color: null,
};
},
computed: {
isValidColor() {
return validateHexColor(this.color);
......@@ -95,18 +62,6 @@ export default {
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: {
onSubmit() {
const { name, description, color } = this;
......@@ -129,69 +84,62 @@ export default {
};
</script>
<template>
<div class="gl-border-t-1 gl-border-t-solid gl-border-t-gray-100">
<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
:label="$options.i18n.titleInputLabel"
:invalid-feedback="$options.i18n.titleInputInvalid"
:state="isValidName"
data-testid="name-input-group"
>
<template #description>
<gl-sprintf :message="$options.i18n.titleInputDescription">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
<template #link="{ content }">
<gl-link :href="scopedLabelsHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</template>
<gl-form @submit.prevent="onSubmit">
<gl-form-group
:label="$options.i18n.titleInputLabel"
:invalid-feedback="$options.i18n.titleInputInvalid"
:state="isValidName"
data-testid="name-input-group"
>
<template #description>
<gl-sprintf :message="$options.i18n.titleInputDescription">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
<gl-form-input :value="name" data-testid="name-input" @input="name = $event" />
</gl-form-group>
<template #link="{ content }">
<gl-link :href="scopedLabelsHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</template>
<gl-form-group
:label="$options.i18n.descriptionInputLabel"
:invalid-feedback="$options.i18n.descriptionInputInvalid"
:state="isValidDescription"
data-testid="description-input-group"
>
<gl-form-input
:value="description"
data-testid="description-input"
@input="description = $event"
/>
</gl-form-group>
<gl-form-input :value="name" data-testid="name-input" @input="$emit('update:name', $event)" />
</gl-form-group>
<color-picker
:value="color"
:label="$options.i18n.colorInputLabel"
:state="isValidColor"
@input="color = $event"
<gl-form-group
:label="$options.i18n.descriptionInputLabel"
:invalid-feedback="$options.i18n.descriptionInputInvalid"
:state="isValidDescription"
data-testid="description-input-group"
>
<gl-form-input
:value="description"
data-testid="description-input"
@input="$emit('update:description', $event)"
/>
</gl-form-group>
<color-picker
:value="color"
:label="$options.i18n.colorInputLabel"
:state="isValidColor"
@input="$emit('update:color', $event)"
/>
<div
class="gl-display-flex gl-justify-content-space-between gl-pt-5 gl-border-t-1 gl-border-t-solid gl-border-t-gray-100"
<div
class="gl-display-flex gl-justify-content-space-between gl-pt-5 gl-border-t-1 gl-border-t-solid gl-border-t-gray-100"
>
<gl-button
type="submit"
variant="success"
class="js-no-auto-disable"
data-testid="submit-btn"
:disabled="disableSubmitBtn"
>{{ $options.i18n.submitBtnText }}</gl-button
>
<gl-button
type="submit"
variant="success"
class="js-no-auto-disable"
data-testid="submit-btn"
:disabled="disableSubmitBtn"
>{{ $options.i18n.submitBtnText }}</gl-button
>
<gl-button :href="groupEditPath" data-testid="cancel-btn">{{
$options.i18n.cancelBtnText
}}</gl-button>
</div>
</gl-form>
</div>
<gl-button :href="groupEditPath" data-testid="cancel-btn">{{
$options.i18n.cancelBtnText
}}</gl-button>
</div>
</gl-form>
</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';
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 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 * as Sentry from '~/sentry/wrapper';
......@@ -17,7 +19,7 @@ localVue.use(VueApollo);
jest.mock('~/lib/utils/url_utility');
describe('Form', () => {
describe('CreateForm', () => {
let wrapper;
const sentryError = new Error('Network error');
const sentrySaveError = new Error('Invalid values given');
......@@ -32,6 +34,7 @@ describe('Form', () => {
const createWithErrors = jest.fn().mockResolvedValue(errorCreateResponse);
const findForm = () => wrapper.findComponent(SharedForm);
const findFormStatus = () => wrapper.findComponent(FormStatus);
function createMockApolloProvider(requestHandlers) {
localVue.use(VueApollo);
......@@ -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(() => {
wrapper.destroy();
});
......@@ -56,9 +70,8 @@ describe('Form', () => {
wrapper = createComponent();
});
it('passes the loading state to the form', () => {
expect(findForm().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
it('passes the loading state to the form status', () => {
expect(findFormStatus().props('loading')).toBe(false);
});
});
......@@ -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');
wrapper = createComponent([[createComplianceFrameworkMutation, createWithNetworkErrors]]);
await waitForPromises();
findForm().vm.$emit('submit', { name, description, color });
await waitForPromises();
await submitForm(name, description, color);
expect(createWithNetworkErrors).toHaveBeenCalledWith(creationProps);
expect(findForm().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
expect(findFormStatus().props('loading')).toBe(false);
expect(visitUrl).not.toHaveBeenCalled();
expect(findForm().props('error')).toBe(
'Unable to save this compliance framework. Please try again',
);
expect(findFormStatus().props('error')).toBe(SAVE_ERROR);
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');
wrapper = createComponent([[createComplianceFrameworkMutation, createWithErrors]]);
await waitForPromises();
findForm().vm.$emit('submit', { name, description, color });
await waitForPromises();
await submitForm(name, description, color);
expect(createWithErrors).toHaveBeenCalledWith(creationProps);
expect(findForm().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
expect(findFormStatus().props('loading')).toBe(false);
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);
});
it('saves inputted values and redirects', async () => {
wrapper = createComponent([[createComplianceFrameworkMutation, create]]);
await waitForPromises();
findForm().vm.$emit('submit', { name, description, color });
await waitForPromises();
await submitForm(name, description, color);
expect(create).toHaveBeenCalledWith(creationProps);
expect(findForm().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
expect(findFormStatus().props('loading')).toBe(false);
expect(visitUrl).toHaveBeenCalledWith(propsData.groupEditPath);
});
});
......
......@@ -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 EditForm from 'ee/groups/settings/compliance_frameworks/components/edit_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 * as Sentry from '~/sentry/wrapper';
......@@ -24,12 +26,12 @@ localVue.use(VueApollo);
jest.mock('~/lib/utils/url_utility');
describe('Form', () => {
describe('EditForm', () => {
let wrapper;
const sentryError = new Error('Network error');
const sentrySaveError = new Error('Invalid values given');
const propsData = {
graphqlFieldName: 'field',
graphqlFieldName: 'ComplianceManagement::Framework',
groupPath: 'group-1',
groupEditPath: 'group-1/edit',
id: '1',
......@@ -46,6 +48,7 @@ describe('Form', () => {
const updateWithErrors = jest.fn().mockResolvedValue(errorUpdateResponse);
const findForm = () => wrapper.findComponent(SharedForm);
const findFormStatus = () => wrapper.findComponent(FormStatus);
function createMockApolloProvider(requestHandlers) {
localVue.use(VueApollo);
......@@ -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(() => {
wrapper.destroy();
});
......@@ -70,9 +84,8 @@ describe('Form', () => {
wrapper = createComponent([[getComplianceFrameworkQuery, fetchLoading]]);
});
it('passes the loading state to the form', () => {
expect(findForm().props('loading')).toBe(true);
expect(findForm().props('renderForm')).toBe(false);
it('passes the loading state to the form status', () => {
expect(findFormStatus().props('loading')).toBe(true);
});
});
......@@ -83,39 +96,37 @@ describe('Form', () => {
await waitForPromises();
expect(fetchOne).toHaveBeenCalledTimes(1);
expect(findForm().props('complianceFramework')).toMatchObject(frameworkFoundResponse);
expect(findForm().props('renderForm')).toBe(true);
expect(findForm().props()).toMatchObject({
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');
wrapper = createComponent([[getComplianceFrameworkQuery, fetchEmpty]]);
await waitForPromises();
expect(fetchEmpty).toHaveBeenCalledTimes(1);
expect(findForm().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(false);
expect(findForm().props('error')).toBe(
'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'),
);
expect(findFormStatus().props('loading')).toBe(false);
expect(findFormStatus().props('error')).toBe(FETCH_ERROR);
expect(Sentry.captureException.mock.calls[0][0]).toStrictEqual(new Error(FETCH_ERROR));
});
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');
wrapper = createComponent([[getComplianceFrameworkQuery, fetchWithErrors]]);
await waitForPromises();
expect(fetchWithErrors).toHaveBeenCalledTimes(1);
expect(findForm().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(false);
expect(findForm().props('error')).toBe(
'Error fetching compliance frameworks data. Please refresh the page',
);
expect(findFormStatus().props('loading')).toBe(false);
expect(findForm().exists()).toBe(false);
expect(findFormStatus().props('error')).toBe(FETCH_ERROR);
expect(Sentry.captureException.mock.calls[0][0].networkError).toBe(sentryError);
});
});
......@@ -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');
wrapper = createComponent([
[getComplianceFrameworkQuery, fetchOne],
[updateComplianceFrameworkMutation, updateWithNetworkErrors],
]);
await waitForPromises();
findForm().vm.$emit('submit', { name, description, color });
await waitForPromises();
await submitForm(name, description, color);
expect(updateWithNetworkErrors).toHaveBeenCalledWith(updateProps);
expect(findForm().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
expect(findFormStatus().props('loading')).toBe(false);
expect(visitUrl).not.toHaveBeenCalled();
expect(findForm().props('error')).toBe(
'Unable to save this compliance framework. Please try again',
);
expect(findFormStatus().props('error')).toBe(SAVE_ERROR);
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');
wrapper = createComponent([
[getComplianceFrameworkQuery, fetchOne],
[updateComplianceFrameworkMutation, updateWithErrors],
]);
await waitForPromises();
findForm().vm.$emit('submit', { name, description, color });
await waitForPromises();
await submitForm(name, description, color);
expect(updateWithErrors).toHaveBeenCalledWith(updateProps);
expect(findForm().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
expect(findFormStatus().props('loading')).toBe(false);
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);
});
......@@ -181,13 +184,10 @@ describe('Form', () => {
[updateComplianceFrameworkMutation, update],
]);
await waitForPromises();
findForm().vm.$emit('submit', { name, description, color });
await waitForPromises();
await submitForm(name, description, color);
expect(update).toHaveBeenCalledWith(updateProps);
expect(findForm().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
expect(findFormStatus().props('loading')).toBe(false);
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 { GlFormGroup } from 'jest/registry/shared/stubs';
import SharedForm from 'ee/groups/settings/compliance_frameworks/components/shared_form.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;
const defaultPropsData = { groupEditPath: 'group-1' };
const findAlert = () => wrapper.findComponent(GlAlert);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findForm = () => wrapper.findComponent(GlForm);
const findNameGroup = () => wrapper.find('[data-testid="name-input-group"]');
const findNameInput = () => wrapper.find('[data-testid="name-input"]');
......@@ -29,59 +27,26 @@ describe('Form', () => {
...props,
},
stubs: {
GlLoadingIcon,
GlFormGroup,
GlSprintf,
},
});
}
beforeAll(() => {
gon.suggested_label_colors = suggestedLabelColors;
});
afterEach(() => {
if (wrapper) {
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', () => {
it('shows the correct input and button fields', () => {
wrapper = createComponent();
expect(findLoadingIcon().exists()).toBe(false);
expect(findNameInput()).toExist();
expect(findDescriptionInput()).toExist();
expect(findColorPicker()).toExist();
......@@ -97,19 +62,14 @@ describe('Form', () => {
});
describe('Validation', () => {
it('throws an error if the provided compliance framework is invalid', () => {
expect(SharedForm.props.complianceFramework.validator({ foo: 'bar' })).toBe(false);
});
it.each`
name | validity
${null} | ${null}
${''} | ${false}
${'foobar'} | ${true}
`('sends the correct state to the name input group', async ({ name, validity }) => {
wrapper = createComponent();
`('sets the correct state to the name input group', ({ name, validity }) => {
wrapper = createComponent({ name });
await findNameInput().vm.$emit('input', name);
expect(findNameGroup().props('state')).toBe(validity);
});
......@@ -118,15 +78,11 @@ describe('Form', () => {
${null} | ${null}
${''} | ${false}
${'foobar'} | ${true}
`(
'sends the correct state to the description input group',
async ({ description, validity }) => {
wrapper = createComponent();
`('sets the correct state to the description input group', ({ description, validity }) => {
wrapper = createComponent({ description });
await findDescriptionInput().vm.$emit('input', description);
expect(findDescriptionGroup().props('state')).toBe(validity);
},
);
expect(findDescriptionGroup().props('state')).toBe(validity);
});
it.each`
color | validity
......@@ -136,12 +92,10 @@ describe('Form', () => {
${'#00'} | ${false}
${'#000'} | ${true}
${'#000000'} | ${true}
`('sends the correct state to the color picker', async ({ color, validity }) => {
wrapper = createComponent();
const colorPicker = findColorPicker();
`('sets the correct state to the color picker', ({ color, validity }) => {
wrapper = createComponent({ color });
await colorPicker.vm.$emit('input', color);
expect(colorPicker.props('state')).toBe(validity);
expect(findColorPicker().props('state')).toBe(validity);
});
it.each`
......@@ -154,12 +108,8 @@ describe('Form', () => {
${'Foo'} | ${'Bar'} | ${'#000'} | ${undefined}
`(
'should set the submit buttons disabled attribute to $disabled',
async ({ name, description, color, disabled }) => {
wrapper = createComponent();
await findNameInput().vm.$emit('input', name);
await findDescriptionInput().vm.$emit('input', description);
await findColorPicker().vm.$emit('input', color);
({ name, description, color, disabled }) => {
wrapper = createComponent({ name, description, color });
expect(findSubmitBtn().attributes('disabled')).toBe(disabled);
},
......@@ -167,50 +117,31 @@ describe('Form', () => {
});
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();
expect(findNameInput().attributes('value')).toBe(undefined);
expect(findDescriptionInput().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(findDescriptionInput().attributes('value')).toBe(frameworkFoundResponse.description);
expect(findColorPicker().attributes('value')).toBe(frameworkFoundResponse.color);
expect(findNameInput().attributes('value')).toBe(name);
expect(findDescriptionInput().attributes('value')).toBe(description);
expect(findColorPicker().attributes('value')).toBe(color);
});
});
describe('On form submission', () => {
it('emits the entered form data', async () => {
wrapper = createComponent();
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');
it('emits a submit event', async () => {
const { name, description, color } = frameworkFoundResponse;
wrapper = createComponent({ name, description, color });
await findForm().vm.$emit('submit', { preventDefault: () => {} });
expect(wrapper.emitted('submit')).toHaveLength(1);
expect(wrapper.emitted('submit')[0]).toEqual([
{ name: 'Foo', description: 'Bar', color: '#000' },
]);
expect(wrapper.emitted('submit')[0]).toEqual([{ name, description, color }]);
});
});
});
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