Commit d6dc0201 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch 'nfriend-reuse-edit-release-for-new' into 'master'

Reuse the "Edit Release" form for release creation

See merge request gitlab-org/gitlab!36466
parents 9abe46ab 35f17fe0
...@@ -3,7 +3,6 @@ import { mapState, mapActions, mapGetters } from 'vuex'; ...@@ -3,7 +3,6 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui'; import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { BACK_URL_PARAM } from '~/releases/constants'; import { BACK_URL_PARAM } from '~/releases/constants';
import { getParameterByName } from '~/lib/utils/common_utils'; import { getParameterByName } from '~/lib/utils/common_utils';
import AssetLinksForm from './asset_links_form.vue'; import AssetLinksForm from './asset_links_form.vue';
...@@ -22,9 +21,6 @@ export default { ...@@ -22,9 +21,6 @@ export default {
MilestoneCombobox, MilestoneCombobox,
TagField, TagField,
}, },
directives: {
autofocusonshow,
},
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
computed: { computed: {
...mapState('detail', [ ...mapState('detail', [
...@@ -40,9 +36,9 @@ export default { ...@@ -40,9 +36,9 @@ export default {
'manageMilestonesPath', 'manageMilestonesPath',
'projectId', 'projectId',
]), ]),
...mapGetters('detail', ['isValid']), ...mapGetters('detail', ['isValid', 'isExistingRelease']),
showForm() { showForm() {
return !this.isFetchingRelease && !this.fetchError; return Boolean(!this.isFetchingRelease && !this.fetchError && this.release);
}, },
subtitleText() { subtitleText() {
return sprintf( return sprintf(
...@@ -86,6 +82,9 @@ export default { ...@@ -86,6 +82,9 @@ export default {
showAssetLinksForm() { showAssetLinksForm() {
return this.glFeatures.releaseAssetLinkEditing; return this.glFeatures.releaseAssetLinkEditing;
}, },
saveButtonLabel() {
return this.isExistingRelease ? __('Save changes') : __('Create release');
},
isSaveChangesDisabled() { isSaveChangesDisabled() {
return this.isUpdatingRelease || !this.isValid; return this.isUpdatingRelease || !this.isValid;
}, },
...@@ -102,13 +101,17 @@ export default { ...@@ -102,13 +101,17 @@ export default {
]; ];
}, },
}, },
created() { mounted() {
this.fetchRelease(); // eslint-disable-next-line promise/catch-or-return
this.initializeRelease().then(() => {
// Focus the first non-disabled input element
this.$el.querySelector('input:enabled').focus();
});
}, },
methods: { methods: {
...mapActions('detail', [ ...mapActions('detail', [
'fetchRelease', 'initializeRelease',
'updateRelease', 'saveRelease',
'updateReleaseTitle', 'updateReleaseTitle',
'updateReleaseNotes', 'updateReleaseNotes',
'updateReleaseMilestones', 'updateReleaseMilestones',
...@@ -119,7 +122,7 @@ export default { ...@@ -119,7 +122,7 @@ export default {
<template> <template>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<p class="pt-3 js-subtitle-text" v-html="subtitleText"></p> <p class="pt-3 js-subtitle-text" v-html="subtitleText"></p>
<form v-if="showForm" @submit.prevent="updateRelease()"> <form v-if="showForm" @submit.prevent="saveRelease()">
<tag-field /> <tag-field />
<gl-form-group> <gl-form-group>
<label for="release-title">{{ __('Release title') }}</label> <label for="release-title">{{ __('Release title') }}</label>
...@@ -127,8 +130,6 @@ export default { ...@@ -127,8 +130,6 @@ export default {
id="release-title" id="release-title"
ref="releaseTitleInput" ref="releaseTitleInput"
v-model="releaseTitle" v-model="releaseTitle"
v-autofocusonshow
autofocus
type="text" type="text"
class="form-control" class="form-control"
/> />
...@@ -162,8 +163,8 @@ export default { ...@@ -162,8 +163,8 @@ export default {
data-supports-quick-actions="false" data-supports-quick-actions="false"
:aria-label="__('Release notes')" :aria-label="__('Release notes')"
:placeholder="__('Write your release notes or drag your files here…')" :placeholder="__('Write your release notes or drag your files here…')"
@keydown.meta.enter="updateRelease()" @keydown.meta.enter="saveRelease()"
@keydown.ctrl.enter="updateRelease()" @keydown.ctrl.enter="saveRelease()"
></textarea> ></textarea>
</template> </template>
</markdown-field> </markdown-field>
...@@ -178,10 +179,11 @@ export default { ...@@ -178,10 +179,11 @@ export default {
category="primary" category="primary"
variant="success" variant="success"
type="submit" type="submit"
:aria-label="__('Save changes')"
:disabled="isSaveChangesDisabled" :disabled="isSaveChangesDisabled"
>{{ __('Save changes') }}</gl-button data-testid="submit-button"
> >
{{ saveButtonLabel }}
</gl-button>
<gl-button :href="cancelPath" class="js-cancel-button">{{ __('Cancel') }}</gl-button> <gl-button :href="cancelPath" class="js-cancel-button">{{ __('Cancel') }}</gl-button>
</div> </div>
</form> </form>
......
...@@ -3,76 +3,114 @@ import api from '~/api'; ...@@ -3,76 +3,114 @@ import api from '~/api';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import { import { releaseToApiJson, apiJsonToRelease } from '~/releases/util';
convertObjectPropsToCamelCase,
convertObjectPropsToSnakeCase, export const initializeRelease = ({ commit, dispatch, getters }) => {
} from '~/lib/utils/common_utils'; if (getters.isExistingRelease) {
// When editing an existing release,
export const requestRelease = ({ commit }) => commit(types.REQUEST_RELEASE); // fetch the release object from the API
export const receiveReleaseSuccess = ({ commit }, data) => return dispatch('fetchRelease');
commit(types.RECEIVE_RELEASE_SUCCESS, data); }
export const receiveReleaseError = ({ commit }, error) => {
commit(types.RECEIVE_RELEASE_ERROR, error); // When creating a new release, initialize the
createFlash(s__('Release|Something went wrong while getting the release details')); // store with an empty release object
commit(types.INITIALIZE_EMPTY_RELEASE);
return Promise.resolve();
}; };
export const fetchRelease = ({ dispatch, state }) => { export const fetchRelease = ({ commit, state }) => {
dispatch('requestRelease'); commit(types.REQUEST_RELEASE);
return api return api
.release(state.projectId, state.tagName) .release(state.projectId, state.tagName)
.then(({ data }) => { .then(({ data }) => {
const release = { commit(types.RECEIVE_RELEASE_SUCCESS, apiJsonToRelease(data));
...data,
milestones: data.milestones || [],
};
dispatch('receiveReleaseSuccess', convertObjectPropsToCamelCase(release, { deep: true }));
}) })
.catch(error => { .catch(error => {
dispatch('receiveReleaseError', error); commit(types.RECEIVE_RELEASE_ERROR, error);
createFlash(s__('Release|Something went wrong while getting the release details'));
}); });
}; };
export const updateReleaseTagName = ({ commit }, tagName) => export const updateReleaseTagName = ({ commit }, tagName) =>
commit(types.UPDATE_RELEASE_TAG_NAME, tagName); commit(types.UPDATE_RELEASE_TAG_NAME, tagName);
export const updateCreateFrom = ({ commit }, createFrom) => export const updateCreateFrom = ({ commit }, createFrom) =>
commit(types.UPDATE_CREATE_FROM, createFrom); commit(types.UPDATE_CREATE_FROM, createFrom);
export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title); export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title);
export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes); export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
export const updateReleaseMilestones = ({ commit }, milestones) => export const updateReleaseMilestones = ({ commit }, milestones) =>
commit(types.UPDATE_RELEASE_MILESTONES, milestones); commit(types.UPDATE_RELEASE_MILESTONES, milestones);
export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE); export const addEmptyAssetLink = ({ commit }) => {
export const receiveUpdateReleaseSuccess = ({ commit, state, rootState }) => { commit(types.ADD_EMPTY_ASSET_LINK);
commit(types.RECEIVE_UPDATE_RELEASE_SUCCESS);
redirectTo(
rootState.featureFlags.releaseShowPage ? state.release._links.self : state.releasesPagePath,
);
}; };
export const receiveUpdateReleaseError = ({ commit }, error) => {
commit(types.RECEIVE_UPDATE_RELEASE_ERROR, error); export const updateAssetLinkUrl = ({ commit }, { linkIdToUpdate, newUrl }) => {
createFlash(s__('Release|Something went wrong while saving the release details')); commit(types.UPDATE_ASSET_LINK_URL, { linkIdToUpdate, newUrl });
};
export const updateAssetLinkName = ({ commit }, { linkIdToUpdate, newName }) => {
commit(types.UPDATE_ASSET_LINK_NAME, { linkIdToUpdate, newName });
};
export const updateAssetLinkType = ({ commit }, { linkIdToUpdate, newType }) => {
commit(types.UPDATE_ASSET_LINK_TYPE, { linkIdToUpdate, newType });
};
export const removeAssetLink = ({ commit }, linkIdToRemove) => {
commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
}; };
export const updateRelease = ({ dispatch, state, getters }) => { export const receiveSaveReleaseSuccess = ({ commit, state, rootState }, release) => {
dispatch('requestUpdateRelease'); commit(types.RECEIVE_SAVE_RELEASE_SUCCESS);
redirectTo(rootState.featureFlags.releaseShowPage ? release._links.self : state.releasesPagePath);
};
const { release } = state; export const saveRelease = ({ commit, dispatch, getters }) => {
const milestones = release.milestones ? release.milestones.map(milestone => milestone.title) : []; commit(types.REQUEST_SAVE_RELEASE);
const updatedRelease = convertObjectPropsToSnakeCase( dispatch(getters.isExistingRelease ? 'updateRelease' : 'createRelease');
};
export const createRelease = ({ commit, dispatch, state, getters }) => {
const apiJson = releaseToApiJson(
{ {
name: release.name, ...state.release,
description: release.description, assets: {
milestones, links: getters.releaseLinksToCreate,
},
}, },
{ deep: true }, state.createFrom,
); );
return api
.createRelease(state.projectId, apiJson)
.then(({ data }) => {
dispatch('receiveSaveReleaseSuccess', apiJsonToRelease(data));
})
.catch(error => {
commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
createFlash(s__('Release|Something went wrong while creating a new release'));
});
};
export const updateRelease = ({ commit, dispatch, state, getters }) => {
const apiJson = releaseToApiJson({
...state.release,
assets: {
links: getters.releaseLinksToCreate,
},
});
let updatedRelease = null;
return ( return (
api api
.updateRelease(state.projectId, state.tagName, updatedRelease) .updateRelease(state.projectId, state.tagName, apiJson)
/** /**
* Currently, we delete all existing links and then * Currently, we delete all existing links and then
...@@ -90,54 +128,31 @@ export const updateRelease = ({ dispatch, state, getters }) => { ...@@ -90,54 +128,31 @@ export const updateRelease = ({ dispatch, state, getters }) => {
* https://gitlab.com/gitlab-org/gitlab/-/issues/208702 * https://gitlab.com/gitlab-org/gitlab/-/issues/208702
* is closed. * is closed.
*/ */
.then(({ data }) => {
// Save this response since we need it later in the Promise chain
updatedRelease = data;
.then(() => {
// Delete all links currently associated with this Release // Delete all links currently associated with this Release
return Promise.all( return Promise.all(
getters.releaseLinksToDelete.map(l => getters.releaseLinksToDelete.map(l =>
api.deleteReleaseLink(state.projectId, release.tagName, l.id), api.deleteReleaseLink(state.projectId, state.release.tagName, l.id),
), ),
); );
}) })
.then(() => { .then(() => {
// Create a new link for each link in the form // Create a new link for each link in the form
return Promise.all( return Promise.all(
getters.releaseLinksToCreate.map(l => apiJson.assets.links.map(l =>
api.createReleaseLink( api.createReleaseLink(state.projectId, state.release.tagName, l),
state.projectId,
release.tagName,
convertObjectPropsToSnakeCase(l, { deep: true }),
),
), ),
); );
}) })
.then(() => dispatch('receiveUpdateReleaseSuccess')) .then(() => {
dispatch('receiveSaveReleaseSuccess', apiJsonToRelease(updatedRelease));
})
.catch(error => { .catch(error => {
dispatch('receiveUpdateReleaseError', error); commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
createFlash(s__('Release|Something went wrong while saving the release details'));
}) })
); );
}; };
export const navigateToReleasesPage = ({ state }) => {
redirectTo(state.releasesPagePath);
};
export const addEmptyAssetLink = ({ commit }) => {
commit(types.ADD_EMPTY_ASSET_LINK);
};
export const updateAssetLinkUrl = ({ commit }, { linkIdToUpdate, newUrl }) => {
commit(types.UPDATE_ASSET_LINK_URL, { linkIdToUpdate, newUrl });
};
export const updateAssetLinkName = ({ commit }, { linkIdToUpdate, newName }) => {
commit(types.UPDATE_ASSET_LINK_NAME, { linkIdToUpdate, newName });
};
export const updateAssetLinkType = ({ commit }, { linkIdToUpdate, newType }) => {
commit(types.UPDATE_ASSET_LINK_TYPE, { linkIdToUpdate, newType });
};
export const removeAssetLink = ({ commit }, linkIdToRemove) => {
commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
};
...@@ -6,7 +6,7 @@ import { hasContent } from '~/lib/utils/text_utility'; ...@@ -6,7 +6,7 @@ import { hasContent } from '~/lib/utils/text_utility';
* `false` if the app is creating a new release. * `false` if the app is creating a new release.
*/ */
export const isExistingRelease = state => { export const isExistingRelease = state => {
return Boolean(state.originalRelease); return Boolean(state.tagName);
}; };
/** /**
......
export const INITIALIZE_EMPTY_RELEASE = 'INITIALIZE_EMPTY_RELEASE';
export const REQUEST_RELEASE = 'REQUEST_RELEASE'; export const REQUEST_RELEASE = 'REQUEST_RELEASE';
export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS'; export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS';
export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR'; export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
...@@ -8,9 +10,9 @@ export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE'; ...@@ -8,9 +10,9 @@ export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE';
export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES'; export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
export const UPDATE_RELEASE_MILESTONES = 'UPDATE_RELEASE_MILESTONES'; export const UPDATE_RELEASE_MILESTONES = 'UPDATE_RELEASE_MILESTONES';
export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE'; export const REQUEST_SAVE_RELEASE = 'REQUEST_SAVE_RELEASE';
export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS'; export const RECEIVE_SAVE_RELEASE_SUCCESS = 'RECEIVE_SAVE_RELEASE_SUCCESS';
export const RECEIVE_UPDATE_RELEASE_ERROR = 'RECEIVE_UPDATE_RELEASE_ERROR'; export const RECEIVE_SAVE_RELEASE_ERROR = 'RECEIVE_SAVE_RELEASE_ERROR';
export const ADD_EMPTY_ASSET_LINK = 'ADD_EMPTY_ASSET_LINK'; export const ADD_EMPTY_ASSET_LINK = 'ADD_EMPTY_ASSET_LINK';
export const UPDATE_ASSET_LINK_URL = 'UPDATE_ASSET_LINK_URL'; export const UPDATE_ASSET_LINK_URL = 'UPDATE_ASSET_LINK_URL';
......
...@@ -7,6 +7,18 @@ const findReleaseLink = (release, id) => { ...@@ -7,6 +7,18 @@ const findReleaseLink = (release, id) => {
}; };
export default { export default {
[types.INITIALIZE_EMPTY_RELEASE](state) {
state.release = {
tagName: null,
name: '',
description: '',
milestones: [],
assets: {
links: [],
},
};
},
[types.REQUEST_RELEASE](state) { [types.REQUEST_RELEASE](state) {
state.isFetchingRelease = true; state.isFetchingRelease = true;
}, },
...@@ -39,14 +51,14 @@ export default { ...@@ -39,14 +51,14 @@ export default {
state.release.milestones = milestones; state.release.milestones = milestones;
}, },
[types.REQUEST_UPDATE_RELEASE](state) { [types.REQUEST_SAVE_RELEASE](state) {
state.isUpdatingRelease = true; state.isUpdatingRelease = true;
}, },
[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state) { [types.RECEIVE_SAVE_RELEASE_SUCCESS](state) {
state.updateError = undefined; state.updateError = undefined;
state.isUpdatingRelease = false; state.isUpdatingRelease = false;
}, },
[types.RECEIVE_UPDATE_RELEASE_ERROR](state, error) { [types.RECEIVE_SAVE_RELEASE_ERROR](state, error) {
state.updateError = error; state.updateError = error;
state.isUpdatingRelease = false; state.isUpdatingRelease = false;
}, },
......
import {
convertObjectPropsToCamelCase,
convertObjectPropsToSnakeCase,
} from '~/lib/utils/common_utils';
/**
* Converts a release object into a JSON object that can sent to the public
* API to create or update a release.
* @param {Object} release The release object to convert
* @param {string} createFrom The ref to create a new tag from, if necessary
*/
export const releaseToApiJson = (release, createFrom = null) => {
const milestones = release.milestones ? release.milestones.map(milestone => milestone.title) : [];
return convertObjectPropsToSnakeCase(
{
tagName: release.tagName,
ref: createFrom,
name: release.name,
description: release.description,
milestones,
assets: release.assets,
},
{ deep: true },
);
};
/**
* Converts a JSON release object returned by the Release API
* into the structure this Vue application can work with.
* @param {Object} json The JSON object received from the release API
*/
export const apiJsonToRelease = json => {
const release = convertObjectPropsToCamelCase(json, { deep: true });
release.milestones = release.milestones || [];
return release;
};
...@@ -7093,6 +7093,9 @@ msgstr "" ...@@ -7093,6 +7093,9 @@ msgstr ""
msgid "Create project label" msgid "Create project label"
msgstr "" msgstr ""
msgid "Create release"
msgstr ""
msgid "Create requirement" msgid "Create requirement"
msgstr "" msgstr ""
...@@ -20099,6 +20102,9 @@ msgstr "" ...@@ -20099,6 +20102,9 @@ msgstr ""
msgid "Releases|New Release" msgid "Releases|New Release"
msgstr "" msgstr ""
msgid "Release|Something went wrong while creating a new release"
msgstr ""
msgid "Release|Something went wrong while getting the release details" msgid "Release|Something went wrong while getting the release details"
msgstr "" msgstr ""
......
...@@ -27,8 +27,8 @@ describe('Release edit/new component', () => { ...@@ -27,8 +27,8 @@ describe('Release edit/new component', () => {
}; };
actions = { actions = {
fetchRelease: jest.fn(), initializeRelease: jest.fn(),
updateRelease: jest.fn(), saveRelease: jest.fn(),
addEmptyAssetLink: jest.fn(), addEmptyAssetLink: jest.fn(),
}; };
...@@ -64,6 +64,8 @@ describe('Release edit/new component', () => { ...@@ -64,6 +64,8 @@ describe('Release edit/new component', () => {
glFeatures: featureFlags, glFeatures: featureFlags,
}, },
}); });
wrapper.element.querySelectorAll('input').forEach(input => jest.spyOn(input, 'focus'));
}; };
beforeEach(() => { beforeEach(() => {
...@@ -87,8 +89,18 @@ describe('Release edit/new component', () => { ...@@ -87,8 +89,18 @@ describe('Release edit/new component', () => {
factory(); factory();
}); });
it('calls fetchRelease when the component is created', () => { it('calls initializeRelease when the component is created', () => {
expect(actions.fetchRelease).toHaveBeenCalledTimes(1); expect(actions.initializeRelease).toHaveBeenCalledTimes(1);
});
it('focuses the first non-disabled input element once the page is shown', () => {
const firstEnabledInput = wrapper.element.querySelector('input:enabled');
const allInputs = wrapper.element.querySelectorAll('input');
allInputs.forEach(input => {
const expectedFocusCalls = input === firstEnabledInput ? 1 : 0;
expect(input.focus).toHaveBeenCalledTimes(expectedFocusCalls);
});
}); });
it('renders the description text at the top of the page', () => { it('renders the description text at the top of the page', () => {
...@@ -109,9 +121,9 @@ describe('Release edit/new component', () => { ...@@ -109,9 +121,9 @@ describe('Release edit/new component', () => {
expect(findSubmitButton().attributes('type')).toBe('submit'); expect(findSubmitButton().attributes('type')).toBe('submit');
}); });
it('calls updateRelease when the form is submitted', () => { it('calls saveRelease when the form is submitted', () => {
wrapper.find('form').trigger('submit'); wrapper.find('form').trigger('submit');
expect(actions.updateRelease).toHaveBeenCalledTimes(1); expect(actions.saveRelease).toHaveBeenCalledTimes(1);
}); });
}); });
...@@ -143,6 +155,34 @@ describe('Release edit/new component', () => { ...@@ -143,6 +155,34 @@ describe('Release edit/new component', () => {
}); });
}); });
describe('when creating a new release', () => {
beforeEach(() => {
factory({
store: {
modules: {
detail: {
getters: {
isExistingRelease: () => false,
},
},
},
},
});
});
it('renders the submit button with the text "Create release"', () => {
expect(findSubmitButton().text()).toBe('Create release');
});
});
describe('when editing an existing release', () => {
beforeEach(factory);
it('renders the submit button with the text "Save changes"', () => {
expect(findSubmitButton().text()).toBe('Save changes');
});
});
describe('asset links form', () => { describe('asset links form', () => {
const findAssetLinksForm = () => wrapper.find(AssetLinksForm); const findAssetLinksForm = () => wrapper.find(AssetLinksForm);
......
...@@ -9,14 +9,14 @@ describe('releases/components/tag_field', () => { ...@@ -9,14 +9,14 @@ describe('releases/components/tag_field', () => {
let store; let store;
let wrapper; let wrapper;
const createComponent = ({ originalRelease }) => { const createComponent = ({ tagName }) => {
store = createStore({ store = createStore({
modules: { modules: {
detail: createDetailModule({}), detail: createDetailModule({}),
}, },
}); });
store.state.detail.originalRelease = originalRelease; store.state.detail.tagName = tagName;
wrapper = shallowMount(TagField, { store }); wrapper = shallowMount(TagField, { store });
}; };
...@@ -31,8 +31,7 @@ describe('releases/components/tag_field', () => { ...@@ -31,8 +31,7 @@ describe('releases/components/tag_field', () => {
describe('when an existing release is being edited', () => { describe('when an existing release is being edited', () => {
beforeEach(() => { beforeEach(() => {
const originalRelease = { name: 'Version 1.0' }; createComponent({ tagName: 'v1.0' });
createComponent({ originalRelease });
}); });
it('renders the TagFieldExisting component', () => { it('renders the TagFieldExisting component', () => {
...@@ -46,7 +45,7 @@ describe('releases/components/tag_field', () => { ...@@ -46,7 +45,7 @@ describe('releases/components/tag_field', () => {
describe('when a new release is being created', () => { describe('when a new release is being created', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ originalRelease: null }); createComponent({ tagName: null });
}); });
it('renders the TagFieldNew component', () => { it('renders the TagFieldNew component', () => {
......
import axios from 'axios'; import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import { cloneDeep, merge } from 'lodash'; import { cloneDeep } from 'lodash';
import * as actions from '~/releases/stores/modules/detail/actions'; import * as actions from '~/releases/stores/modules/detail/actions';
import * as types from '~/releases/stores/modules/detail/mutation_types'; import * as types from '~/releases/stores/modules/detail/mutation_types';
import { release as originalRelease } from '../../../mock_data'; import { release as originalRelease } from '../../../mock_data';
...@@ -10,7 +10,9 @@ import createFlash from '~/flash'; ...@@ -10,7 +10,9 @@ import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import api from '~/api'; import api from '~/api';
import httpStatus from '~/lib/utils/http_status';
import { ASSET_LINK_TYPE } from '~/releases/constants'; import { ASSET_LINK_TYPE } from '~/releases/constants';
import { releaseToApiJson, apiJsonToRelease } from '~/releases/util';
jest.mock('~/flash', () => jest.fn()); jest.mock('~/flash', () => jest.fn());
...@@ -25,15 +27,26 @@ describe('Release detail actions', () => { ...@@ -25,15 +27,26 @@ describe('Release detail actions', () => {
let mock; let mock;
let error; let error;
beforeEach(() => { const setupState = (updates = {}) => {
state = createState({ const getters = {
isExistingRelease: true,
};
state = {
...createState({
projectId: '18', projectId: '18',
tagName: 'v1.3', tagName: release.tag_name,
releasesPagePath: 'path/to/releases/page', releasesPagePath: 'path/to/releases/page',
markdownDocsPath: 'path/to/markdown/docs', markdownDocsPath: 'path/to/markdown/docs',
markdownPreviewPath: 'path/to/markdown/preview', markdownPreviewPath: 'path/to/markdown/preview',
updateReleaseApiDocsPath: 'path/to/api/docs', updateReleaseApiDocsPath: 'path/to/api/docs',
}); }),
...getters,
...updates,
};
};
beforeEach(() => {
release = cloneDeep(originalRelease); release = cloneDeep(originalRelease);
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
gon.api_version = 'v4'; gon.api_version = 'v4';
...@@ -45,73 +58,109 @@ describe('Release detail actions', () => { ...@@ -45,73 +58,109 @@ describe('Release detail actions', () => {
mock.restore(); mock.restore();
}); });
describe('requestRelease', () => { describe('when creating a new release', () => {
it(`commits ${types.REQUEST_RELEASE}`, () => beforeEach(() => {
testAction(actions.requestRelease, undefined, state, [{ type: types.REQUEST_RELEASE }])); setupState({ isExistingRelease: false });
}); });
describe('receiveReleaseSuccess', () => { describe('initializeRelease', () => {
it(`commits ${types.RECEIVE_RELEASE_SUCCESS}`, () => it(`commits ${types.INITIALIZE_EMPTY_RELEASE}`, () => {
testAction(actions.receiveReleaseSuccess, release, state, [ testAction(actions.initializeRelease, undefined, state, [
{ type: types.RECEIVE_RELEASE_SUCCESS, payload: release }, { type: types.INITIALIZE_EMPTY_RELEASE },
])); ]);
});
}); });
describe('receiveReleaseError', () => { describe('saveRelease', () => {
it(`commits ${types.RECEIVE_RELEASE_ERROR}`, () => it(`commits ${types.REQUEST_SAVE_RELEASE} and then dispatched "createRelease"`, () => {
testAction(actions.receiveReleaseError, error, state, [ testAction(
{ type: types.RECEIVE_RELEASE_ERROR, payload: error }, actions.saveRelease,
])); undefined,
state,
[{ type: types.REQUEST_SAVE_RELEASE }],
[{ type: 'createRelease' }],
);
});
});
});
it('shows a flash with an error message', () => { describe('when editing an existing release', () => {
actions.receiveReleaseError({ commit: jest.fn() }, error); beforeEach(setupState);
expect(createFlash).toHaveBeenCalledTimes(1); describe('initializeRelease', () => {
expect(createFlash).toHaveBeenCalledWith( it('dispatches "fetchRelease"', () => {
'Something went wrong while getting the release details', testAction(actions.initializeRelease, undefined, state, [], [{ type: 'fetchRelease' }]);
});
});
describe('saveRelease', () => {
it(`commits ${types.REQUEST_SAVE_RELEASE} and then dispatched "updateRelease"`, () => {
testAction(
actions.saveRelease,
undefined,
state,
[{ type: types.REQUEST_SAVE_RELEASE }],
[{ type: 'updateRelease' }],
); );
}); });
}); });
});
describe('actions that behave the same whether creating a new release or editing an existing release', () => {
beforeEach(setupState);
describe('fetchRelease', () => { describe('fetchRelease', () => {
let getReleaseUrl; let getReleaseUrl;
beforeEach(() => { beforeEach(() => {
state.projectId = '18';
state.tagName = 'v1.3';
getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`; getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
}); });
it(`dispatches requestRelease and receiveReleaseSuccess with the camel-case'd release object`, () => { describe('when the network request to the Release API is successful', () => {
mock.onGet(getReleaseUrl).replyOnce(200, release); beforeEach(() => {
mock.onGet(getReleaseUrl).replyOnce(httpStatus.OK, release);
});
return testAction( it(`commits ${types.REQUEST_RELEASE} and then commits ${types.RECEIVE_RELEASE_SUCCESS} with the converted release object`, () => {
actions.fetchRelease, return testAction(actions.fetchRelease, undefined, state, [
undefined,
state,
[],
[
{ type: 'requestRelease' },
{ {
type: 'receiveReleaseSuccess', type: types.REQUEST_RELEASE,
payload: convertObjectPropsToCamelCase(release, { deep: true }),
}, },
], {
); type: types.RECEIVE_RELEASE_SUCCESS,
payload: apiJsonToRelease(release, { deep: true }),
},
]);
});
}); });
it(`dispatches requestRelease and receiveReleaseError with an error object`, () => { describe('when the network request to the Release API fails', () => {
mock.onGet(getReleaseUrl).replyOnce(500); beforeEach(() => {
mock.onGet(getReleaseUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
});
return testAction( it(`commits ${types.REQUEST_RELEASE} and then commits ${types.RECEIVE_RELEASE_ERROR} with an error object`, () => {
actions.fetchRelease, return testAction(actions.fetchRelease, undefined, state, [
undefined, {
state, type: types.REQUEST_RELEASE,
[], },
[{ type: 'requestRelease' }, { type: 'receiveReleaseError', payload: expect.anything() }], {
type: types.RECEIVE_RELEASE_ERROR,
payload: expect.any(Error),
},
]);
});
it(`shows a flash message`, () => {
return actions.fetchRelease({ commit: jest.fn(), state }).then(() => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(
'Something went wrong while getting the release details',
); );
}); });
}); });
});
});
describe('updateReleaseTagName', () => { describe('updateReleaseTagName', () => {
it(`commits ${types.UPDATE_RELEASE_TAG_NAME} with the updated tag name`, () => { it(`commits ${types.UPDATE_RELEASE_TAG_NAME} with the updated tag name`, () => {
...@@ -149,6 +198,23 @@ describe('Release detail actions', () => { ...@@ -149,6 +198,23 @@ describe('Release detail actions', () => {
}); });
}); });
describe('updateReleaseMilestones', () => {
it(`commits ${types.UPDATE_RELEASE_MILESTONES} with the updated release milestones`, () => {
const newReleaseMilestones = ['v0.0', 'v0.1'];
return testAction(actions.updateReleaseMilestones, newReleaseMilestones, state, [
{ type: types.UPDATE_RELEASE_MILESTONES, payload: newReleaseMilestones },
]);
});
});
describe('addEmptyAssetLink', () => {
it(`commits ${types.ADD_EMPTY_ASSET_LINK}`, () => {
return testAction(actions.addEmptyAssetLink, undefined, state, [
{ type: types.ADD_EMPTY_ASSET_LINK },
]);
});
});
describe('updateAssetLinkUrl', () => { describe('updateAssetLinkUrl', () => {
it(`commits ${types.UPDATE_ASSET_LINK_URL} with the updated link URL`, () => { it(`commits ${types.UPDATE_ASSET_LINK_URL} with the updated link URL`, () => {
const params = { const params = {
...@@ -197,85 +263,133 @@ describe('Release detail actions', () => { ...@@ -197,85 +263,133 @@ describe('Release detail actions', () => {
}); });
}); });
describe('updateReleaseMilestones', () => { describe('receiveSaveReleaseSuccess', () => {
it(`commits ${types.UPDATE_RELEASE_MILESTONES} with the updated release milestones`, () => { it(`commits ${types.RECEIVE_SAVE_RELEASE_SUCCESS}`, () =>
const newReleaseMilestones = ['v0.0', 'v0.1']; testAction(actions.receiveSaveReleaseSuccess, undefined, { ...state, featureFlags: {} }, [
return testAction(actions.updateReleaseMilestones, newReleaseMilestones, state, [ { type: types.RECEIVE_SAVE_RELEASE_SUCCESS },
{ type: types.UPDATE_RELEASE_MILESTONES, payload: newReleaseMilestones }, ]));
]);
describe('when the releaseShowPage feature flag is enabled', () => {
beforeEach(() => {
const rootState = { featureFlags: { releaseShowPage: true } };
actions.receiveSaveReleaseSuccess({ commit: jest.fn(), state, rootState }, release);
});
it("redirects to the release's dedicated page", () => {
expect(redirectTo).toHaveBeenCalledTimes(1);
expect(redirectTo).toHaveBeenCalledWith(release._links.self);
}); });
}); });
describe('requestUpdateRelease', () => { describe('when the releaseShowPage feature flag is disabled', () => {
it(`commits ${types.REQUEST_UPDATE_RELEASE}`, () => beforeEach(() => {
testAction(actions.requestUpdateRelease, undefined, state, [ const rootState = { featureFlags: { releaseShowPage: false } };
{ type: types.REQUEST_UPDATE_RELEASE }, actions.receiveSaveReleaseSuccess({ commit: jest.fn(), state, rootState }, release);
]));
}); });
describe('receiveUpdateReleaseSuccess', () => { it("redirects to the project's main Releases page", () => {
it(`commits ${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () => expect(redirectTo).toHaveBeenCalledTimes(1);
testAction(actions.receiveUpdateReleaseSuccess, undefined, { ...state, featureFlags: {} }, [ expect(redirectTo).toHaveBeenCalledWith(state.releasesPagePath);
{ type: types.RECEIVE_UPDATE_RELEASE_SUCCESS }, });
])); });
});
it('redirects to the releases page if releaseShowPage feature flag is enabled', () => { describe('createRelease', () => {
const rootState = { featureFlags: { releaseShowPage: true } }; let createReleaseUrl;
const updatedState = merge({}, state, { let releaseLinksToCreate;
releasesPagePath: 'path/to/releases/page',
release: { beforeEach(() => {
_links: { const camelCasedRelease = convertObjectPropsToCamelCase(release);
self: 'path/to/self',
}, releaseLinksToCreate = camelCasedRelease.assets.links.slice(0, 1);
},
setupState({
release: camelCasedRelease,
releaseLinksToCreate,
}); });
actions.receiveUpdateReleaseSuccess({ commit: jest.fn(), state: updatedState, rootState }); createReleaseUrl = `/api/v4/projects/${state.projectId}/releases`;
});
expect(redirectTo).toHaveBeenCalledTimes(1); describe('when the network request to the Release API is successful', () => {
expect(redirectTo).toHaveBeenCalledWith(updatedState.release._links.self); beforeEach(() => {
const expectedRelease = releaseToApiJson({
...state.release,
assets: {
links: releaseLinksToCreate,
},
}); });
describe('when the releaseShowPage feature flag is disabled', () => {}); mock.onPost(createReleaseUrl, expectedRelease).replyOnce(httpStatus.CREATED, release);
}); });
describe('receiveUpdateReleaseError', () => { it(`dispatches "receiveSaveReleaseSuccess" with the converted release object`, () => {
it(`commits ${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () => return testAction(
testAction(actions.receiveUpdateReleaseError, error, state, [ actions.createRelease,
{ type: types.RECEIVE_UPDATE_RELEASE_ERROR, payload: error }, undefined,
])); state,
[],
[
{
type: 'receiveSaveReleaseSuccess',
payload: apiJsonToRelease(release, { deep: true }),
},
],
);
});
});
describe('when the network request to the Release API fails', () => {
beforeEach(() => {
mock.onPost(createReleaseUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
});
it('shows a flash with an error message', () => { it(`commits ${types.RECEIVE_SAVE_RELEASE_ERROR} with an error object`, () => {
actions.receiveUpdateReleaseError({ commit: jest.fn() }, error); return testAction(actions.createRelease, undefined, state, [
{
type: types.RECEIVE_SAVE_RELEASE_ERROR,
payload: expect.any(Error),
},
]);
});
it(`shows a flash message`, () => {
return actions
.createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} })
.then(() => {
expect(createFlash).toHaveBeenCalledTimes(1); expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith( expect(createFlash).toHaveBeenCalledWith(
'Something went wrong while saving the release details', 'Something went wrong while creating a new release',
); );
}); });
}); });
});
});
describe('updateRelease', () => { describe('updateRelease', () => {
let getters; let getters;
let dispatch; let dispatch;
let commit;
let callOrder; let callOrder;
beforeEach(() => { beforeEach(() => {
state.release = convertObjectPropsToCamelCase(release);
state.projectId = '18';
state.tagName = state.release.tagName;
getters = { getters = {
releaseLinksToDelete: [{ id: '1' }, { id: '2' }], releaseLinksToDelete: [{ id: '1' }, { id: '2' }],
releaseLinksToCreate: [{ id: 'new-link-1' }, { id: 'new-link-2' }], releaseLinksToCreate: [{ id: 'new-link-1' }, { id: 'new-link-2' }],
}; };
setupState({
release: convertObjectPropsToCamelCase(release),
...getters,
});
dispatch = jest.fn(); dispatch = jest.fn();
commit = jest.fn();
callOrder = []; callOrder = [];
jest.spyOn(api, 'updateRelease').mockImplementation(() => { jest.spyOn(api, 'updateRelease').mockImplementation(() => {
callOrder.push('updateRelease'); callOrder.push('updateRelease');
return Promise.resolve(); return Promise.resolve({ data: release });
}); });
jest.spyOn(api, 'deleteReleaseLink').mockImplementation(() => { jest.spyOn(api, 'deleteReleaseLink').mockImplementation(() => {
callOrder.push('deleteReleaseLink'); callOrder.push('deleteReleaseLink');
...@@ -287,22 +401,11 @@ describe('Release detail actions', () => { ...@@ -287,22 +401,11 @@ describe('Release detail actions', () => {
}); });
}); });
it('dispatches requestUpdateRelease and receiveUpdateReleaseSuccess', () => { describe('when the network request to the Release API is successful', () => {
return actions.updateRelease({ dispatch, state, getters }).then(() => { it('dispatches receiveSaveReleaseSuccess', () => {
return actions.updateRelease({ commit, dispatch, state, getters }).then(() => {
expect(dispatch.mock.calls).toEqual([ expect(dispatch.mock.calls).toEqual([
['requestUpdateRelease'], ['receiveSaveReleaseSuccess', apiJsonToRelease(release)],
['receiveUpdateReleaseSuccess'],
]);
});
});
it('dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object', () => {
jest.spyOn(api, 'updateRelease').mockRejectedValue(error);
return actions.updateRelease({ dispatch, state, getters }).then(() => {
expect(dispatch.mock.calls).toEqual([
['requestUpdateRelease'],
['receiveUpdateReleaseError', error],
]); ]);
}); });
}); });
...@@ -321,15 +424,18 @@ describe('Release detail actions', () => { ...@@ -321,15 +424,18 @@ describe('Release detail actions', () => {
[ [
state.projectId, state.projectId,
state.tagName, state.tagName,
{ releaseToApiJson({
name: state.release.name, ...state.release,
description: state.release.description, assets: {
milestones: state.release.milestones.map(milestone => milestone.title), links: getters.releaseLinksToCreate,
}, },
}),
], ],
]); ]);
expect(api.deleteReleaseLink).toHaveBeenCalledTimes(getters.releaseLinksToDelete.length); expect(api.deleteReleaseLink).toHaveBeenCalledTimes(
getters.releaseLinksToDelete.length,
);
getters.releaseLinksToDelete.forEach(link => { getters.releaseLinksToDelete.forEach(link => {
expect(api.deleteReleaseLink).toHaveBeenCalledWith( expect(api.deleteReleaseLink).toHaveBeenCalledWith(
state.projectId, state.projectId,
...@@ -338,9 +444,38 @@ describe('Release detail actions', () => { ...@@ -338,9 +444,38 @@ describe('Release detail actions', () => {
); );
}); });
expect(api.createReleaseLink).toHaveBeenCalledTimes(getters.releaseLinksToCreate.length); expect(api.createReleaseLink).toHaveBeenCalledTimes(
getters.releaseLinksToCreate.length,
);
getters.releaseLinksToCreate.forEach(link => { getters.releaseLinksToCreate.forEach(link => {
expect(api.createReleaseLink).toHaveBeenCalledWith(state.projectId, state.tagName, link); expect(api.createReleaseLink).toHaveBeenCalledWith(
state.projectId,
state.tagName,
link,
);
});
});
});
});
describe('when the network request to the Release API fails', () => {
beforeEach(() => {
jest.spyOn(api, 'updateRelease').mockRejectedValue(error);
});
it('dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object', () => {
return actions.updateRelease({ commit, dispatch, state, getters }).then(() => {
expect(commit.mock.calls).toEqual([[types.RECEIVE_SAVE_RELEASE_ERROR, error]]);
});
});
it('shows a flash message', () => {
return actions.updateRelease({ commit, dispatch, state, getters }).then(() => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(
'Something went wrong while saving the release details',
);
});
}); });
}); });
}); });
......
...@@ -3,13 +3,13 @@ import * as getters from '~/releases/stores/modules/detail/getters'; ...@@ -3,13 +3,13 @@ import * as getters from '~/releases/stores/modules/detail/getters';
describe('Release detail getters', () => { describe('Release detail getters', () => {
describe('isExistingRelease', () => { describe('isExistingRelease', () => {
it('returns true if the release is an existing release that already exists in the database', () => { it('returns true if the release is an existing release that already exists in the database', () => {
const state = { originalRelease: { name: 'The first release' } }; const state = { tagName: 'test-tag-name' };
expect(getters.isExistingRelease(state)).toBe(true); expect(getters.isExistingRelease(state)).toBe(true);
}); });
it('returns false if the release is a new release that has not yet been saved to the database', () => { it('returns false if the release is a new release that has not yet been saved to the database', () => {
const state = { originalRelease: null }; const state = { tagName: null };
expect(getters.isExistingRelease(state)).toBe(false); expect(getters.isExistingRelease(state)).toBe(false);
}); });
......
...@@ -21,6 +21,22 @@ describe('Release detail mutations', () => { ...@@ -21,6 +21,22 @@ describe('Release detail mutations', () => {
release = convertObjectPropsToCamelCase(originalRelease); release = convertObjectPropsToCamelCase(originalRelease);
}); });
describe(`${types.INITIALIZE_EMPTY_RELEASE}`, () => {
it('set state.release to an empty release object', () => {
mutations[types.INITIALIZE_EMPTY_RELEASE](state);
expect(state.release).toEqual({
tagName: null,
name: '',
description: '',
milestones: [],
assets: {
links: [],
},
});
});
});
describe(`${types.REQUEST_RELEASE}`, () => { describe(`${types.REQUEST_RELEASE}`, () => {
it('set state.isFetchingRelease to true', () => { it('set state.isFetchingRelease to true', () => {
mutations[types.REQUEST_RELEASE](state); mutations[types.REQUEST_RELEASE](state);
...@@ -96,17 +112,17 @@ describe('Release detail mutations', () => { ...@@ -96,17 +112,17 @@ describe('Release detail mutations', () => {
}); });
}); });
describe(`${types.REQUEST_UPDATE_RELEASE}`, () => { describe(`${types.REQUEST_SAVE_RELEASE}`, () => {
it('set state.isUpdatingRelease to true', () => { it('set state.isUpdatingRelease to true', () => {
mutations[types.REQUEST_UPDATE_RELEASE](state); mutations[types.REQUEST_SAVE_RELEASE](state);
expect(state.isUpdatingRelease).toBe(true); expect(state.isUpdatingRelease).toBe(true);
}); });
}); });
describe(`${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () => { describe(`${types.RECEIVE_SAVE_RELEASE_SUCCESS}`, () => {
it('handles a successful response from the server', () => { it('handles a successful response from the server', () => {
mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, release); mutations[types.RECEIVE_SAVE_RELEASE_SUCCESS](state, release);
expect(state.updateError).toBeUndefined(); expect(state.updateError).toBeUndefined();
...@@ -114,10 +130,10 @@ describe('Release detail mutations', () => { ...@@ -114,10 +130,10 @@ describe('Release detail mutations', () => {
}); });
}); });
describe(`${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () => { describe(`${types.RECEIVE_SAVE_RELEASE_ERROR}`, () => {
it('handles an unsuccessful response from the server', () => { it('handles an unsuccessful response from the server', () => {
const error = { message: 'An error occurred!' }; const error = { message: 'An error occurred!' };
mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](state, error); mutations[types.RECEIVE_SAVE_RELEASE_ERROR](state, error);
expect(state.isUpdatingRelease).toBe(false); expect(state.isUpdatingRelease).toBe(false);
......
import { releaseToApiJson, apiJsonToRelease } from '~/releases/util';
describe('releases/util.js', () => {
describe('releaseToApiJson', () => {
it('converts a release JavaScript object into JSON that the Release API can accept', () => {
const release = {
tagName: 'tag-name',
name: 'Release name',
description: 'Release description',
milestones: [{ id: 1, title: '13.2' }, { id: 2, title: '13.3' }],
assets: {
links: [{ url: 'https://gitlab.example.com/link', linkType: 'other' }],
},
};
const expectedJson = {
tag_name: 'tag-name',
ref: null,
name: 'Release name',
description: 'Release description',
milestones: ['13.2', '13.3'],
assets: {
links: [{ url: 'https://gitlab.example.com/link', link_type: 'other' }],
},
};
expect(releaseToApiJson(release)).toEqual(expectedJson);
});
describe('when createFrom is provided', () => {
it('adds the provided createFrom ref to the JSON as a "ref" property', () => {
const createFrom = 'main';
const release = {};
const expectedJson = {
ref: createFrom,
};
expect(releaseToApiJson(release, createFrom)).toMatchObject(expectedJson);
});
});
describe('when release.milestones is falsy', () => {
it('includes a "milestone" property in the returned result as an empty array', () => {
const release = {};
const expectedJson = {
milestones: [],
};
expect(releaseToApiJson(release)).toMatchObject(expectedJson);
});
});
});
describe('apiJsonToRelease', () => {
it('converts JSON received from the Release API into an object usable by the Vue application', () => {
const json = {
tag_name: 'tag-name',
assets: {
links: [
{
link_type: 'other',
},
],
},
};
const expectedRelease = {
tagName: 'tag-name',
assets: {
links: [
{
linkType: 'other',
},
],
},
milestones: [],
};
expect(apiJsonToRelease(json)).toEqual(expectedRelease);
});
});
});
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