Commit 49ae16b0 authored by Jake Burden's avatar Jake Burden Committed by Andrew Fontaine

Populate combobox with group milestones

Fetchs data from group milestones API
Searches with the group search API
Adds group milestone results section
Check if group_milestones_project_releases is available
parent e59dd19e
...@@ -39,6 +39,16 @@ export default { ...@@ -39,6 +39,16 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
groupId: {
type: String,
required: false,
default: '',
},
groupMilestonesAvailable: {
type: Boolean,
required: false,
default: false,
},
extraLinks: { extraLinks: {
type: Array, type: Array,
default: () => [], default: () => [],
...@@ -56,12 +66,13 @@ export default { ...@@ -56,12 +66,13 @@ export default {
noMilestone: s__('MilestoneCombobox|No milestone'), noMilestone: s__('MilestoneCombobox|No milestone'),
noResultsLabel: s__('MilestoneCombobox|No matching results'), noResultsLabel: s__('MilestoneCombobox|No matching results'),
searchMilestones: s__('MilestoneCombobox|Search Milestones'), searchMilestones: s__('MilestoneCombobox|Search Milestones'),
searhErrorMessage: s__('MilestoneCombobox|An error occurred while searching for milestones'), searchErrorMessage: s__('MilestoneCombobox|An error occurred while searching for milestones'),
projectMilestones: s__('MilestoneCombobox|Project milestones'), projectMilestones: s__('MilestoneCombobox|Project milestones'),
groupMilestones: s__('MilestoneCombobox|Group milestones'),
}, },
computed: { computed: {
...mapState(['matches', 'selectedMilestones']), ...mapState(['matches', 'selectedMilestones']),
...mapGetters(['isLoading']), ...mapGetters(['isLoading', 'groupMilestonesEnabled']),
selectedMilestonesLabel() { selectedMilestonesLabel() {
const { selectedMilestones } = this; const { selectedMilestones } = this;
const firstMilestoneName = selectedMilestones[0]; const firstMilestoneName = selectedMilestones[0];
...@@ -85,8 +96,14 @@ export default { ...@@ -85,8 +96,14 @@ export default {
this.matches.projectMilestones.totalCount > 0 || this.matches.projectMilestones.error, this.matches.projectMilestones.totalCount > 0 || this.matches.projectMilestones.error,
); );
}, },
showGroupMilestoneSection() {
return (
this.groupMilestonesEnabled &&
Boolean(this.matches.groupMilestones.totalCount > 0 || this.matches.groupMilestones.error)
);
},
showNoResults() { showNoResults() {
return !this.showProjectMilestoneSection; return !this.showProjectMilestoneSection && !this.showGroupMilestoneSection;
}, },
}, },
watch: { watch: {
...@@ -115,11 +132,15 @@ export default { ...@@ -115,11 +132,15 @@ export default {
}, SEARCH_DEBOUNCE_MS); }, SEARCH_DEBOUNCE_MS);
this.setProjectId(this.projectId); this.setProjectId(this.projectId);
this.setGroupId(this.groupId);
this.setGroupMilestonesAvailable(this.groupMilestonesAvailable);
this.fetchMilestones(); this.fetchMilestones();
}, },
methods: { methods: {
...mapActions([ ...mapActions([
'setProjectId', 'setProjectId',
'setGroupId',
'setGroupMilestonesAvailable',
'setSelectedMilestones', 'setSelectedMilestones',
'clearSelectedMilestones', 'clearSelectedMilestones',
'toggleMilestones', 'toggleMilestones',
...@@ -194,15 +215,28 @@ export default { ...@@ -194,15 +215,28 @@ export default {
</template> </template>
<template v-else> <template v-else>
<milestone-results-section <milestone-results-section
v-if="showProjectMilestoneSection"
:section-title="$options.translations.projectMilestones" :section-title="$options.translations.projectMilestones"
:total-count="matches.projectMilestones.totalCount" :total-count="matches.projectMilestones.totalCount"
:items="matches.projectMilestones.list" :items="matches.projectMilestones.list"
:selected-milestones="selectedMilestones" :selected-milestones="selectedMilestones"
:error="matches.projectMilestones.error" :error="matches.projectMilestones.error"
:error-message="$options.translations.searhErrorMessage" :error-message="$options.translations.searchErrorMessage"
data-testid="project-milestones-section" data-testid="project-milestones-section"
@selected="selectMilestone($event)" @selected="selectMilestone($event)"
/> />
<milestone-results-section
v-if="showGroupMilestoneSection"
:section-title="$options.translations.groupMilestones"
:total-count="matches.groupMilestones.totalCount"
:items="matches.groupMilestones.list"
:selected-milestones="selectedMilestones"
:error="matches.groupMilestones.error"
:error-message="$options.translations.searchErrorMessage"
data-testid="group-milestones-section"
@selected="selectMilestone($event)"
/>
</template> </template>
<gl-dropdown-item <gl-dropdown-item
v-for="(item, idx) in extraLinks" v-for="(item, idx) in extraLinks"
......
...@@ -2,6 +2,9 @@ import Api from '~/api'; ...@@ -2,6 +2,9 @@ import Api from '~/api';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const setProjectId = ({ commit }, projectId) => commit(types.SET_PROJECT_ID, projectId); export const setProjectId = ({ commit }, projectId) => commit(types.SET_PROJECT_ID, projectId);
export const setGroupId = ({ commit }, groupId) => commit(types.SET_GROUP_ID, groupId);
export const setGroupMilestonesAvailable = ({ commit }, groupMilestonesAvailable) =>
commit(types.SET_GROUP_MILESTONES_AVAILABLE, groupMilestonesAvailable);
export const setSelectedMilestones = ({ commit }, selectedMilestones) => export const setSelectedMilestones = ({ commit }, selectedMilestones) =>
commit(types.SET_SELECTED_MILESTONES, selectedMilestones); commit(types.SET_SELECTED_MILESTONES, selectedMilestones);
...@@ -18,13 +21,23 @@ export const toggleMilestones = ({ commit, state }, selectedMilestone) => { ...@@ -18,13 +21,23 @@ export const toggleMilestones = ({ commit, state }, selectedMilestone) => {
} }
}; };
export const search = ({ dispatch, commit }, searchQuery) => { export const search = ({ dispatch, commit, getters }, searchQuery) => {
commit(types.SET_SEARCH_QUERY, searchQuery); commit(types.SET_SEARCH_QUERY, searchQuery);
dispatch('searchMilestones'); dispatch('searchProjectMilestones');
if (getters.groupMilestonesEnabled) {
dispatch('searchGroupMilestones');
}
};
export const fetchMilestones = ({ dispatch, getters }) => {
dispatch('fetchProjectMilestones');
if (getters.groupMilestonesEnabled) {
dispatch('fetchGroupMilestones');
}
}; };
export const fetchMilestones = ({ commit, state }) => { export const fetchProjectMilestones = ({ commit, state }) => {
commit(types.REQUEST_START); commit(types.REQUEST_START);
Api.projectMilestones(state.projectId) Api.projectMilestones(state.projectId)
...@@ -39,14 +52,29 @@ export const fetchMilestones = ({ commit, state }) => { ...@@ -39,14 +52,29 @@ export const fetchMilestones = ({ commit, state }) => {
}); });
}; };
export const searchMilestones = ({ commit, state }) => { export const fetchGroupMilestones = ({ commit, state }) => {
commit(types.REQUEST_START); commit(types.REQUEST_START);
Api.groupMilestones(state.groupId)
.then(response => {
commit(types.RECEIVE_GROUP_MILESTONES_SUCCESS, response);
})
.catch(error => {
commit(types.RECEIVE_GROUP_MILESTONES_ERROR, error);
})
.finally(() => {
commit(types.REQUEST_FINISH);
});
};
export const searchProjectMilestones = ({ commit, state }) => {
const options = { const options = {
search: state.searchQuery, search: state.searchQuery,
scope: 'milestones', scope: 'milestones',
}; };
commit(types.REQUEST_START);
Api.projectSearch(state.projectId, options) Api.projectSearch(state.projectId, options)
.then(response => { .then(response => {
commit(types.RECEIVE_PROJECT_MILESTONES_SUCCESS, response); commit(types.RECEIVE_PROJECT_MILESTONES_SUCCESS, response);
...@@ -58,3 +86,22 @@ export const searchMilestones = ({ commit, state }) => { ...@@ -58,3 +86,22 @@ export const searchMilestones = ({ commit, state }) => {
commit(types.REQUEST_FINISH); commit(types.REQUEST_FINISH);
}); });
}; };
export const searchGroupMilestones = ({ commit, state }) => {
const options = {
search: state.searchQuery,
};
commit(types.REQUEST_START);
Api.groupMilestones(state.groupId, options)
.then(response => {
commit(types.RECEIVE_GROUP_MILESTONES_SUCCESS, response);
})
.catch(error => {
commit(types.RECEIVE_GROUP_MILESTONES_ERROR, error);
})
.finally(() => {
commit(types.REQUEST_FINISH);
});
};
/** Returns `true` if there is at least one in-progress request */ /** Returns `true` if there is at least one in-progress request */
export const isLoading = ({ requestCount }) => requestCount > 0; export const isLoading = ({ requestCount }) => requestCount > 0;
/** Returns `true` if there is a group ID and group milestones are available */
export const groupMilestonesEnabled = ({ groupId, groupMilestonesAvailable }) =>
Boolean(groupId && groupMilestonesAvailable);
export const SET_PROJECT_ID = 'SET_PROJECT_ID'; export const SET_PROJECT_ID = 'SET_PROJECT_ID';
export const SET_GROUP_ID = 'SET_GROUP_ID';
export const SET_GROUP_MILESTONES_AVAILABLE = 'SET_GROUP_MILESTONES_AVAILABLE';
export const SET_SELECTED_MILESTONES = 'SET_SELECTED_MILESTONES'; export const SET_SELECTED_MILESTONES = 'SET_SELECTED_MILESTONES';
export const CLEAR_SELECTED_MILESTONES = 'CLEAR_SELECTED_MILESTONES'; export const CLEAR_SELECTED_MILESTONES = 'CLEAR_SELECTED_MILESTONES';
...@@ -12,3 +14,6 @@ export const REQUEST_FINISH = 'REQUEST_FINISH'; ...@@ -12,3 +14,6 @@ export const REQUEST_FINISH = 'REQUEST_FINISH';
export const RECEIVE_PROJECT_MILESTONES_SUCCESS = 'RECEIVE_PROJECT_MILESTONES_SUCCESS'; export const RECEIVE_PROJECT_MILESTONES_SUCCESS = 'RECEIVE_PROJECT_MILESTONES_SUCCESS';
export const RECEIVE_PROJECT_MILESTONES_ERROR = 'RECEIVE_PROJECT_MILESTONES_ERROR'; export const RECEIVE_PROJECT_MILESTONES_ERROR = 'RECEIVE_PROJECT_MILESTONES_ERROR';
export const RECEIVE_GROUP_MILESTONES_SUCCESS = 'RECEIVE_GROUP_MILESTONES_SUCCESS';
export const RECEIVE_GROUP_MILESTONES_ERROR = 'RECEIVE_GROUP_MILESTONES_ERROR';
import Vue from 'vue'; import Vue from 'vue';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default { export default {
[types.SET_PROJECT_ID](state, projectId) { [types.SET_PROJECT_ID](state, projectId) {
state.projectId = projectId; state.projectId = projectId;
}, },
[types.SET_GROUP_ID](state, groupId) {
state.groupId = groupId;
},
[types.SET_GROUP_MILESTONES_AVAILABLE](state, groupMilestonesAvailable) {
state.groupMilestonesAvailable = groupMilestonesAvailable;
},
[types.SET_SELECTED_MILESTONES](state, selectedMilestones) { [types.SET_SELECTED_MILESTONES](state, selectedMilestones) {
Vue.set(state, 'selectedMilestones', selectedMilestones); Vue.set(state, 'selectedMilestones', selectedMilestones);
}, },
...@@ -32,7 +37,7 @@ export default { ...@@ -32,7 +37,7 @@ export default {
}, },
[types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response) { [types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response) {
state.matches.projectMilestones = { state.matches.projectMilestones = {
list: convertObjectPropsToCamelCase(response.data).map(({ title }) => ({ title })), list: response.data.map(({ title }) => ({ title })),
totalCount: parseInt(response.headers['x-total'], 10), totalCount: parseInt(response.headers['x-total'], 10),
error: null, error: null,
}; };
...@@ -44,4 +49,18 @@ export default { ...@@ -44,4 +49,18 @@ export default {
error, error,
}; };
}, },
[types.RECEIVE_GROUP_MILESTONES_SUCCESS](state, response) {
state.matches.groupMilestones = {
list: response.data.map(({ title }) => ({ title })),
totalCount: parseInt(response.headers['x-total'], 10),
error: null,
};
},
[types.RECEIVE_GROUP_MILESTONES_ERROR](state, error) {
state.matches.groupMilestones = {
list: [],
totalCount: 0,
error,
};
},
}; };
export default () => ({ export default () => ({
projectId: null, projectId: null,
groupId: null, groupId: null,
groupMilestonesAvailable: false,
searchQuery: '', searchQuery: '',
matches: { matches: {
projectMilestones: { projectMilestones: {
...@@ -8,6 +9,11 @@ export default () => ({ ...@@ -8,6 +9,11 @@ export default () => ({
totalCount: 0, totalCount: 0,
error: null, error: null,
}, },
groupMilestones: {
list: [],
totalCount: 0,
error: null,
},
}, },
selectedMilestones: [], selectedMilestones: [],
requestCount: 0, requestCount: 0,
......
...@@ -34,6 +34,8 @@ export default { ...@@ -34,6 +34,8 @@ export default {
'newMilestonePath', 'newMilestonePath',
'manageMilestonesPath', 'manageMilestonesPath',
'projectId', 'projectId',
'groupId',
'groupMilestonesAvailable',
]), ]),
...mapGetters('detail', ['isValid', 'isExistingRelease']), ...mapGetters('detail', ['isValid', 'isExistingRelease']),
showForm() { showForm() {
...@@ -141,6 +143,8 @@ export default { ...@@ -141,6 +143,8 @@ export default {
<milestone-combobox <milestone-combobox
v-model="releaseMilestones" v-model="releaseMilestones"
:project-id="projectId" :project-id="projectId"
:group-id="groupId"
:group-milestones-available="groupMilestonesAvailable"
:extra-links="milestoneComboboxExtraLinks" :extra-links="milestoneComboboxExtraLinks"
/> />
</div> </div>
......
export default ({ export default ({
projectId, projectId,
groupId,
groupMilestonesAvailable = false,
projectPath, projectPath,
markdownDocsPath, markdownDocsPath,
markdownPreviewPath, markdownPreviewPath,
...@@ -13,6 +15,8 @@ export default ({ ...@@ -13,6 +15,8 @@ export default ({
defaultBranch = null, defaultBranch = null,
}) => ({ }) => ({
projectId, projectId,
groupId,
groupMilestonesAvailable: Boolean(groupMilestonesAvailable),
projectPath, projectPath,
markdownDocsPath, markdownDocsPath,
markdownPreviewPath, markdownPreviewPath,
......
...@@ -51,11 +51,17 @@ module ReleasesHelper ...@@ -51,11 +51,17 @@ module ReleasesHelper
) )
end end
def group_milestone_project_releases_available?(project)
false
end
private private
def new_edit_pages_shared_data def new_edit_pages_shared_data
{ {
project_id: @project.id, project_id: @project.id,
group_id: @project.group&.id,
group_milestones_available: group_milestone_project_releases_available?(@project),
project_path: @project.full_path, project_path: @project.full_path,
markdown_preview_path: preview_markdown_path(@project), markdown_preview_path: preview_markdown_path(@project),
markdown_docs_path: help_page_path('user/markdown'), markdown_docs_path: help_page_path('user/markdown'),
...@@ -66,3 +72,5 @@ module ReleasesHelper ...@@ -66,3 +72,5 @@ module ReleasesHelper
} }
end end
end end
ReleasesHelper.prepend_if_ee('EE::ReleasesHelper')
...@@ -130,6 +130,8 @@ In the interface, to add release notes to an existing Git tag: ...@@ -130,6 +130,8 @@ In the interface, to add release notes to an existing Git tag:
You can associate a release with one or more [project milestones](../milestones/index.md#project-milestones-and-group-milestones). You can associate a release with one or more [project milestones](../milestones/index.md#project-milestones-and-group-milestones).
[GitLab Premium](https://about.gitlab.com/pricing/) customers can specify [group milestones](../milestones/index.md#project-milestones-and-group-milestones) to associate with a release.
You can do this in the user interface, or by including a `milestones` array in your request to You can do this in the user interface, or by including a `milestones` array in your request to
the [Releases API](../../../api/releases/index.md#create-a-release). the [Releases API](../../../api/releases/index.md#create-a-release).
......
# frozen_string_literal: true
module EE
module ReleasesHelper
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
override :group_milestone_project_releases_available?
def group_milestone_project_releases_available?(project)
project.feature_available?(:group_milestone_project_releases).to_s
end
end
end
---
title: Resolve Populate the milestone dropdown combobox on the Release edit/new page
with Group milestones
merge_request: 46027
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ReleasesHelper do
let(:project) { build(:project, namespace: create(:group)) }
let(:release) { create(:release, project: project) }
# rubocop: disable CodeReuse/ActiveRecord
before do
helper.instance_variable_set(:@project, project)
helper.instance_variable_set(:@release, release)
end
# rubocop: enable CodeReuse/ActiveRecord
describe '#group_milestone_project_releases_available?' do
subject { helper.data_for_edit_release_page[:group_milestones_available] }
context 'when group milestones association with project releases is enabled' do
before do
stub_licensed_features(group_milestone_project_releases: true)
end
it { is_expected.to eq("true") }
end
context 'when group milestones association with project releases is disabled' do
before do
stub_licensed_features(group_milestone_project_releases: false)
end
it { is_expected.to eq("false") }
end
end
end
...@@ -17370,6 +17370,9 @@ msgstr "" ...@@ -17370,6 +17370,9 @@ msgstr ""
msgid "MilestoneCombobox|An error occurred while searching for milestones" msgid "MilestoneCombobox|An error occurred while searching for milestones"
msgstr "" msgstr ""
msgid "MilestoneCombobox|Group milestones"
msgstr ""
msgid "MilestoneCombobox|Milestone" msgid "MilestoneCombobox|Milestone"
msgstr "" msgstr ""
......
...@@ -6,7 +6,7 @@ import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; ...@@ -6,7 +6,7 @@ import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { ENTER_KEY } from '~/lib/utils/keys'; import { ENTER_KEY } from '~/lib/utils/keys';
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue'; import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
import { milestones as projectMilestones } from './mock_data'; import { projectMilestones, groupMilestones } from './mock_data';
import createStore from '~/milestones/stores/'; import createStore from '~/milestones/stores/';
const extraLinks = [ const extraLinks = [
...@@ -19,16 +19,21 @@ localVue.use(Vuex); ...@@ -19,16 +19,21 @@ localVue.use(Vuex);
describe('Milestone combobox component', () => { describe('Milestone combobox component', () => {
const projectId = '8'; const projectId = '8';
const groupId = '24';
const groupMilestonesAvailable = true;
const X_TOTAL_HEADER = 'x-total'; const X_TOTAL_HEADER = 'x-total';
let wrapper; let wrapper;
let projectMilestonesApiCallSpy; let projectMilestonesApiCallSpy;
let groupMilestonesApiCallSpy;
let searchApiCallSpy; let searchApiCallSpy;
const createComponent = (props = {}, attrs = {}) => { const createComponent = (props = {}, attrs = {}) => {
wrapper = mount(MilestoneCombobox, { wrapper = mount(MilestoneCombobox, {
propsData: { propsData: {
projectId, projectId,
groupId,
groupMilestonesAvailable,
extraLinks, extraLinks,
value: [], value: [],
...props, ...props,
...@@ -56,6 +61,10 @@ describe('Milestone combobox component', () => { ...@@ -56,6 +61,10 @@ describe('Milestone combobox component', () => {
.fn() .fn()
.mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]); .mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
groupMilestonesApiCallSpy = jest
.fn()
.mockReturnValue([200, groupMilestones, { [X_TOTAL_HEADER]: '6' }]);
searchApiCallSpy = jest searchApiCallSpy = jest
.fn() .fn()
.mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]); .mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
...@@ -64,6 +73,10 @@ describe('Milestone combobox component', () => { ...@@ -64,6 +73,10 @@ describe('Milestone combobox component', () => {
.onGet(`/api/v4/projects/${projectId}/milestones`) .onGet(`/api/v4/projects/${projectId}/milestones`)
.reply(config => projectMilestonesApiCallSpy(config)); .reply(config => projectMilestonesApiCallSpy(config));
mock
.onGet(`/api/v4/groups/${groupId}/milestones`)
.reply(config => groupMilestonesApiCallSpy(config));
mock.onGet(`/api/v4/projects/${projectId}/search`).reply(config => searchApiCallSpy(config)); mock.onGet(`/api/v4/projects/${projectId}/search`).reply(config => searchApiCallSpy(config));
}); });
...@@ -89,6 +102,11 @@ describe('Milestone combobox component', () => { ...@@ -89,6 +102,11 @@ describe('Milestone combobox component', () => {
findProjectMilestonesSection().findAll(GlDropdownItem); findProjectMilestonesSection().findAll(GlDropdownItem);
const findFirstProjectMilestonesDropdownItem = () => findProjectMilestonesDropdownItems().at(0); const findFirstProjectMilestonesDropdownItem = () => findProjectMilestonesDropdownItems().at(0);
const findGroupMilestonesSection = () => wrapper.find('[data-testid="group-milestones-section"]');
const findGroupMilestonesDropdownItems = () =>
findGroupMilestonesSection().findAll(GlDropdownItem);
const findFirstGroupMilestonesDropdownItem = () => findGroupMilestonesDropdownItems().at(0);
// //
// Expecters // Expecters
// //
...@@ -100,6 +118,14 @@ describe('Milestone combobox component', () => { ...@@ -100,6 +118,14 @@ describe('Milestone combobox component', () => {
.includes(s__('MilestoneCombobox|An error occurred while searching for milestones')); .includes(s__('MilestoneCombobox|An error occurred while searching for milestones'));
}; };
const groupMilestoneSectionContainsErrorMessage = () => {
const groupMilestoneSection = findGroupMilestonesSection();
return groupMilestoneSection
.text()
.includes(s__('MilestoneCombobox|An error occurred while searching for milestones'));
};
// //
// Convenience methods // Convenience methods
// //
...@@ -111,19 +137,25 @@ describe('Milestone combobox component', () => { ...@@ -111,19 +137,25 @@ describe('Milestone combobox component', () => {
findFirstProjectMilestonesDropdownItem().vm.$emit('click'); findFirstProjectMilestonesDropdownItem().vm.$emit('click');
}; };
const selectFirstGroupMilestone = () => {
findFirstGroupMilestonesDropdownItem().vm.$emit('click');
};
const waitForRequests = ({ andClearMocks } = { andClearMocks: false }) => const waitForRequests = ({ andClearMocks } = { andClearMocks: false }) =>
axios.waitForAll().then(() => { axios.waitForAll().then(() => {
if (andClearMocks) { if (andClearMocks) {
projectMilestonesApiCallSpy.mockClear(); projectMilestonesApiCallSpy.mockClear();
groupMilestonesApiCallSpy.mockClear();
} }
}); });
describe('initialization behavior', () => { describe('initialization behavior', () => {
beforeEach(createComponent); beforeEach(createComponent);
it('initializes the dropdown with project milestones when mounted', () => { it('initializes the dropdown with milestones when mounted', () => {
return waitForRequests().then(() => { return waitForRequests().then(() => {
expect(projectMilestonesApiCallSpy).toHaveBeenCalledTimes(1); expect(projectMilestonesApiCallSpy).toHaveBeenCalledTimes(1);
expect(groupMilestonesApiCallSpy).toHaveBeenCalledTimes(1);
}); });
}); });
...@@ -166,7 +198,7 @@ describe('Milestone combobox component', () => { ...@@ -166,7 +198,7 @@ describe('Milestone combobox component', () => {
return waitForRequests(); return waitForRequests();
}); });
it('renders the pre-selected project milestones', () => { it('renders the pre-selected milestones', () => {
expect(findButtonContent().text()).toBe('v0.1 + 5 more'); expect(findButtonContent().text()).toBe('v0.1 + 5 more');
}); });
}); });
...@@ -209,6 +241,8 @@ describe('Milestone combobox component', () => { ...@@ -209,6 +241,8 @@ describe('Milestone combobox component', () => {
.fn() .fn()
.mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]); .mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
groupMilestonesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent(); createComponent();
return waitForRequests(); return waitForRequests();
...@@ -288,7 +322,6 @@ describe('Milestone combobox component', () => { ...@@ -288,7 +322,6 @@ describe('Milestone combobox component', () => {
expect(projectMilestoneSectionContainsErrorMessage()).toBe(true); expect(projectMilestoneSectionContainsErrorMessage()).toBe(true);
}); });
}); });
});
describe('selection', () => { describe('selection', () => {
beforeEach(() => { beforeEach(() => {
...@@ -310,14 +343,14 @@ describe('Milestone combobox component', () => { ...@@ -310,14 +343,14 @@ describe('Milestone combobox component', () => {
selectFirstProjectMilestone(); selectFirstProjectMilestone();
return localVue.nextTick().then(() => { await localVue.nextTick();
expect( expect(
findFirstProjectMilestonesDropdownItem() findFirstProjectMilestonesDropdownItem()
.find('span') .find('span')
.classes('selected-item'), .classes('selected-item'),
).toBe(true); ).toBe(true);
}); });
});
describe('when a project milestones is selected', () => { describe('when a project milestones is selected', () => {
beforeEach(() => { beforeEach(() => {
...@@ -351,4 +384,135 @@ describe('Milestone combobox component', () => { ...@@ -351,4 +384,135 @@ describe('Milestone combobox component', () => {
}); });
}); });
}); });
describe('group milestones', () => {
describe('when the group milestones search returns results', () => {
beforeEach(() => {
createComponent();
return waitForRequests();
});
it('renders the group milestones section in the dropdown', () => {
expect(findGroupMilestonesSection().exists()).toBe(true);
});
it('renders the "Group milestones" heading with a total number indicator', () => {
expect(
findGroupMilestonesSection()
.find('[data-testid="milestone-results-section-header"]')
.text(),
).toBe('Group milestones 6');
});
it("does not render an error message in the group milestone section's body", () => {
expect(groupMilestoneSectionContainsErrorMessage()).toBe(false);
});
it('renders each group milestones as a selectable item', () => {
const dropdownItems = findGroupMilestonesDropdownItems();
groupMilestones.forEach((milestone, i) => {
expect(dropdownItems.at(i).text()).toBe(milestone.title);
});
});
});
describe('when the group milestones search returns no results', () => {
beforeEach(() => {
groupMilestonesApiCallSpy = jest
.fn()
.mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
return waitForRequests();
});
it('does not render the group milestones section in the dropdown', () => {
expect(findGroupMilestonesSection().exists()).toBe(false);
});
});
describe('when the group milestones search returns an error', () => {
beforeEach(() => {
groupMilestonesApiCallSpy = jest.fn().mockReturnValue([500]);
searchApiCallSpy = jest.fn().mockReturnValue([500]);
createComponent({ value: [] });
return waitForRequests();
});
it('renders the group milestones section in the dropdown', () => {
expect(findGroupMilestonesSection().exists()).toBe(true);
});
it("renders an error message in the group milestones section's body", () => {
expect(groupMilestoneSectionContainsErrorMessage()).toBe(true);
});
});
describe('selection', () => {
beforeEach(() => {
createComponent();
return waitForRequests();
});
it('renders a checkmark by the selected item', async () => {
selectFirstGroupMilestone();
await localVue.nextTick();
expect(
findFirstGroupMilestonesDropdownItem()
.find('span')
.classes('selected-item'),
).toBe(false);
selectFirstGroupMilestone();
await localVue.nextTick();
expect(
findFirstGroupMilestonesDropdownItem()
.find('span')
.classes('selected-item'),
).toBe(true);
});
describe('when a group milestones is selected', () => {
beforeEach(() => {
createComponent();
groupMilestonesApiCallSpy = jest
.fn()
.mockReturnValue([200, [{ title: 'group-v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
return waitForRequests();
});
it("displays the group milestones name in the dropdown's button", async () => {
selectFirstGroupMilestone();
await localVue.nextTick();
expect(findButtonContent().text()).toBe(s__('MilestoneCombobox|No milestone'));
selectFirstGroupMilestone();
await localVue.nextTick();
expect(findButtonContent().text()).toBe('group-v1.0');
});
it('updates the v-model binding with the group milestone title', () => {
expect(wrapper.vm.value).toEqual([]);
selectFirstGroupMilestone();
expect(wrapper.vm.value).toEqual(['group-v1.0']);
});
});
});
});
});
}); });
export const milestones = [ export const projectMilestones = [
{ {
id: 41, id: 41,
iid: 6, iid: 6,
...@@ -79,4 +79,94 @@ export const milestones = [ ...@@ -79,4 +79,94 @@ export const milestones = [
}, },
]; ];
export default milestones; export const groupMilestones = [
{
id: 141,
iid: 16,
project_id: 8,
group_id: 12,
title: 'group-v0.1',
description: '',
state: 'active',
created_at: '2020-04-04T01:30:40.051Z',
updated_at: '2020-04-04T01:30:40.051Z',
due_date: null,
start_date: null,
web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/6',
},
{
id: 140,
iid: 15,
project_id: 8,
group_id: 12,
title: 'group-v4.0',
description: 'Laboriosam nisi sapiente dolores et magnam nobis ad earum.',
state: 'closed',
created_at: '2020-01-13T19:39:15.191Z',
updated_at: '2020-01-13T19:39:15.191Z',
due_date: null,
start_date: null,
web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/5',
},
{
id: 139,
iid: 14,
project_id: 8,
group_id: 12,
title: 'group-v3.0',
description: 'Necessitatibus illo alias et repellat dolorum assumenda ut.',
state: 'closed',
created_at: '2020-01-13T19:39:15.176Z',
updated_at: '2020-01-13T19:39:15.176Z',
due_date: null,
start_date: null,
web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/4',
},
{
id: 138,
iid: 13,
project_id: 8,
group_id: 12,
title: 'group-v2.0',
description: 'Doloribus qui repudiandae iste sit.',
state: 'closed',
created_at: '2020-01-13T19:39:15.161Z',
updated_at: '2020-01-13T19:39:15.161Z',
due_date: null,
start_date: null,
web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/3',
},
{
id: 137,
iid: 12,
project_id: 8,
group_id: 12,
title: 'group-v1.0',
description: 'Illo sint odio officia ea.',
state: 'closed',
created_at: '2020-01-13T19:39:15.146Z',
updated_at: '2020-01-13T19:39:15.146Z',
due_date: null,
start_date: null,
web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/2',
},
{
id: 136,
iid: 11,
project_id: 8,
group_id: 12,
title: 'group-v0.0',
description: 'Sed quae facilis deleniti at delectus assumenda nobis veritatis.',
state: 'active',
created_at: '2020-01-13T19:39:15.127Z',
updated_at: '2020-01-13T19:39:15.127Z',
due_date: null,
start_date: null,
web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/1',
},
];
export default {
projectMilestones,
groupMilestones,
};
...@@ -4,6 +4,7 @@ import * as actions from '~/milestones/stores/actions'; ...@@ -4,6 +4,7 @@ import * as actions from '~/milestones/stores/actions';
import * as types from '~/milestones/stores/mutation_types'; import * as types from '~/milestones/stores/mutation_types';
let mockProjectMilestonesReturnValue; let mockProjectMilestonesReturnValue;
let mockGroupMilestonesReturnValue;
let mockProjectSearchReturnValue; let mockProjectSearchReturnValue;
jest.mock('~/api', () => ({ jest.mock('~/api', () => ({
...@@ -13,6 +14,7 @@ jest.mock('~/api', () => ({ ...@@ -13,6 +14,7 @@ jest.mock('~/api', () => ({
default: { default: {
projectMilestones: () => mockProjectMilestonesReturnValue, projectMilestones: () => mockProjectMilestonesReturnValue,
projectSearch: () => mockProjectSearchReturnValue, projectSearch: () => mockProjectSearchReturnValue,
groupMilestones: () => mockGroupMilestonesReturnValue,
}, },
})); }));
...@@ -32,6 +34,24 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -32,6 +34,24 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
}); });
describe('setGroupId', () => {
it(`commits ${types.SET_GROUP_ID} with the new group ID`, () => {
const groupId = '123';
testAction(actions.setGroupId, groupId, state, [
{ type: types.SET_GROUP_ID, payload: groupId },
]);
});
});
describe('setGroupMilestonesAvailable', () => {
it(`commits ${types.SET_GROUP_MILESTONES_AVAILABLE} with the boolean indicating if group milestones are available (Premium)`, () => {
state.groupMilestonesAvailable = true;
testAction(actions.setGroupMilestonesAvailable, state.groupMilestonesAvailable, state, [
{ type: types.SET_GROUP_MILESTONES_AVAILABLE, payload: state.groupMilestonesAvailable },
]);
});
});
describe('setSelectedMilestones', () => { describe('setSelectedMilestones', () => {
it(`commits ${types.SET_SELECTED_MILESTONES} with the new selected milestones name`, () => { it(`commits ${types.SET_SELECTED_MILESTONES} with the new selected milestones name`, () => {
const selectedMilestones = ['v1.2.3']; const selectedMilestones = ['v1.2.3'];
...@@ -66,19 +86,38 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -66,19 +86,38 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
describe('search', () => { describe('search', () => {
it(`commits ${types.SET_SEARCH_QUERY} with the new search query`, () => { describe('when project has license to add group milestones', () => {
it(`commits ${types.SET_SEARCH_QUERY} with the new search query to search for project and group milestones`, () => {
const getters = {
groupMilestonesEnabled: () => true,
};
const searchQuery = 'v1.0';
testAction(
actions.search,
searchQuery,
{ ...state, ...getters },
[{ type: types.SET_SEARCH_QUERY, payload: searchQuery }],
[{ type: 'searchProjectMilestones' }, { type: 'searchGroupMilestones' }],
);
});
});
describe('when project does not have license to add group milestones', () => {
it(`commits ${types.SET_SEARCH_QUERY} with the new search query to search for project milestones`, () => {
const searchQuery = 'v1.0'; const searchQuery = 'v1.0';
testAction( testAction(
actions.search, actions.search,
searchQuery, searchQuery,
state, state,
[{ type: types.SET_SEARCH_QUERY, payload: searchQuery }], [{ type: types.SET_SEARCH_QUERY, payload: searchQuery }],
[{ type: 'searchMilestones' }], [{ type: 'searchProjectMilestones' }],
); );
}); });
}); });
});
describe('searchMilestones', () => { describe('searchProjectMilestones', () => {
describe('when the search is successful', () => { describe('when the search is successful', () => {
const projectSearchApiResponse = { data: [{ title: 'v1.0' }] }; const projectSearchApiResponse = { data: [{ title: 'v1.0' }] };
...@@ -87,7 +126,7 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -87,7 +126,7 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => { it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.searchMilestones, undefined, state, [ return testAction(actions.searchProjectMilestones, undefined, state, [
{ type: types.REQUEST_START }, { type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectSearchApiResponse }, { type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectSearchApiResponse },
{ type: types.REQUEST_FINISH }, { type: types.REQUEST_FINISH },
...@@ -103,7 +142,7 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -103,7 +142,7 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => { it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.searchMilestones, undefined, state, [ return testAction(actions.searchProjectMilestones, undefined, state, [
{ type: types.REQUEST_START }, { type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error }, { type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error },
{ type: types.REQUEST_FINISH }, { type: types.REQUEST_FINISH },
...@@ -112,7 +151,71 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -112,7 +151,71 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
}); });
describe('searchGroupMilestones', () => {
describe('when the search is successful', () => {
const groupSearchApiResponse = { data: [{ title: 'group-v1.0' }] };
beforeEach(() => {
mockGroupMilestonesReturnValue = Promise.resolve(groupSearchApiResponse);
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.searchGroupMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_GROUP_MILESTONES_SUCCESS, payload: groupSearchApiResponse },
{ type: types.REQUEST_FINISH },
]);
});
});
describe('when the search fails', () => {
const error = new Error('Something went wrong!');
beforeEach(() => {
mockGroupMilestonesReturnValue = Promise.reject(error);
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.searchGroupMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_GROUP_MILESTONES_ERROR, payload: error },
{ type: types.REQUEST_FINISH },
]);
});
});
});
describe('fetchMilestones', () => { describe('fetchMilestones', () => {
describe('when project has license to add group milestones', () => {
it(`dispatchs fetchProjectMilestones and fetchGroupMilestones`, () => {
const getters = {
groupMilestonesEnabled: () => true,
};
testAction(
actions.fetchMilestones,
undefined,
{ ...state, ...getters },
[],
[{ type: 'fetchProjectMilestones' }, { type: 'fetchGroupMilestones' }],
);
});
});
describe('when project does not have license to add group milestones', () => {
it(`dispatchs fetchProjectMilestones`, () => {
testAction(
actions.fetchMilestones,
undefined,
state,
[],
[{ type: 'fetchProjectMilestones' }],
);
});
});
});
describe('fetchProjectMilestones', () => {
describe('when the fetch is successful', () => { describe('when the fetch is successful', () => {
const projectMilestonesApiResponse = { data: [{ title: 'v1.0' }] }; const projectMilestonesApiResponse = { data: [{ title: 'v1.0' }] };
...@@ -121,7 +224,7 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -121,7 +224,7 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => { it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.fetchMilestones, undefined, state, [ return testAction(actions.fetchProjectMilestones, undefined, state, [
{ type: types.REQUEST_START }, { type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectMilestonesApiResponse }, { type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectMilestonesApiResponse },
{ type: types.REQUEST_FINISH }, { type: types.REQUEST_FINISH },
...@@ -137,7 +240,7 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -137,7 +240,7 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => { it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.fetchMilestones, undefined, state, [ return testAction(actions.fetchProjectMilestones, undefined, state, [
{ type: types.REQUEST_START }, { type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error }, { type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error },
{ type: types.REQUEST_FINISH }, { type: types.REQUEST_FINISH },
...@@ -145,4 +248,38 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -145,4 +248,38 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
}); });
}); });
describe('fetchGroupMilestones', () => {
describe('when the fetch is successful', () => {
const groupMilestonesApiResponse = { data: [{ title: 'group-v1.0' }] };
beforeEach(() => {
mockGroupMilestonesReturnValue = Promise.resolve(groupMilestonesApiResponse);
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.fetchGroupMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_GROUP_MILESTONES_SUCCESS, payload: groupMilestonesApiResponse },
{ type: types.REQUEST_FINISH },
]);
});
});
describe('when the fetch fails', () => {
const error = new Error('Something went wrong!');
beforeEach(() => {
mockGroupMilestonesReturnValue = Promise.reject(error);
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.fetchGroupMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_GROUP_MILESTONES_ERROR, payload: error },
{ type: types.REQUEST_FINISH },
]);
});
});
});
}); });
...@@ -12,4 +12,22 @@ describe('Milestone comboxbox Vuex store getters', () => { ...@@ -12,4 +12,22 @@ describe('Milestone comboxbox Vuex store getters', () => {
expect(getters.isLoading({ requestCount })).toBe(isLoading); expect(getters.isLoading({ requestCount })).toBe(isLoading);
}); });
}); });
describe('groupMilestonesEnabled', () => {
it.each`
groupId | groupMilestonesAvailable | groupMilestonesEnabled
${'1'} | ${true} | ${true}
${'1'} | ${false} | ${false}
${''} | ${true} | ${false}
${''} | ${false} | ${false}
${null} | ${true} | ${false}
`(
'returns true when groupId is a truthy string and groupMilestonesAvailable is true',
({ groupId, groupMilestonesAvailable, groupMilestonesEnabled }) => {
expect(getters.groupMilestonesEnabled({ groupId, groupMilestonesAvailable })).toBe(
groupMilestonesEnabled,
);
},
);
});
}); });
...@@ -14,6 +14,7 @@ describe('Milestones combobox Vuex store mutations', () => { ...@@ -14,6 +14,7 @@ describe('Milestones combobox Vuex store mutations', () => {
expect(state).toEqual({ expect(state).toEqual({
projectId: null, projectId: null,
groupId: null, groupId: null,
groupMilestonesAvailable: false,
searchQuery: '', searchQuery: '',
matches: { matches: {
projectMilestones: { projectMilestones: {
...@@ -21,6 +22,11 @@ describe('Milestones combobox Vuex store mutations', () => { ...@@ -21,6 +22,11 @@ describe('Milestones combobox Vuex store mutations', () => {
totalCount: 0, totalCount: 0,
error: null, error: null,
}, },
groupMilestones: {
list: [],
totalCount: 0,
error: null,
},
}, },
selectedMilestones: [], selectedMilestones: [],
requestCount: 0, requestCount: 0,
...@@ -37,6 +43,24 @@ describe('Milestones combobox Vuex store mutations', () => { ...@@ -37,6 +43,24 @@ describe('Milestones combobox Vuex store mutations', () => {
}); });
}); });
describe(`${types.SET_GROUP_ID}`, () => {
it('updates the group ID', () => {
const newGroupId = '8';
mutations[types.SET_GROUP_ID](state, newGroupId);
expect(state.groupId).toBe(newGroupId);
});
});
describe(`${types.SET_GROUP_MILESTONES_AVAILABLE}`, () => {
it('sets boolean indicating if group milestones are available', () => {
const groupMilestonesAvailable = true;
mutations[types.SET_GROUP_MILESTONES_AVAILABLE](state, groupMilestonesAvailable);
expect(state.groupMilestonesAvailable).toBe(groupMilestonesAvailable);
});
});
describe(`${types.SET_SELECTED_MILESTONES}`, () => { describe(`${types.SET_SELECTED_MILESTONES}`, () => {
it('sets the selected milestones', () => { it('sets the selected milestones', () => {
const selectedMilestones = ['v1.2.3']; const selectedMilestones = ['v1.2.3'];
...@@ -60,7 +84,7 @@ describe('Milestones combobox Vuex store mutations', () => { ...@@ -60,7 +84,7 @@ describe('Milestones combobox Vuex store mutations', () => {
}); });
}); });
describe(`${types.ADD_SELECTED_MILESTONESs}`, () => { describe(`${types.ADD_SELECTED_MILESTONES}`, () => {
it('adds the selected milestones', () => { it('adds the selected milestones', () => {
const selectedMilestone = 'v1.2.3'; const selectedMilestone = 'v1.2.3';
mutations[types.ADD_SELECTED_MILESTONE](state, selectedMilestone); mutations[types.ADD_SELECTED_MILESTONE](state, selectedMilestone);
...@@ -170,4 +194,57 @@ describe('Milestones combobox Vuex store mutations', () => { ...@@ -170,4 +194,57 @@ describe('Milestones combobox Vuex store mutations', () => {
}); });
}); });
}); });
describe(`${types.RECEIVE_GROUP_MILESTONES_SUCCESS}`, () => {
it('updates state.matches.groupMilestones based on the provided API response', () => {
const response = {
data: [
{
title: 'group-0.1',
},
{
title: 'group-0.2',
},
],
headers: {
'x-total': 2,
},
};
mutations[types.RECEIVE_GROUP_MILESTONES_SUCCESS](state, response);
expect(state.matches.groupMilestones).toEqual({
list: [
{
title: 'group-0.1',
},
{
title: 'group-0.2',
},
],
error: null,
totalCount: 2,
});
});
describe(`${types.RECEIVE_GROUP_MILESTONES_ERROR}`, () => {
it('updates state.matches.groupMilestones to an empty state with the error object', () => {
const error = new Error('Something went wrong!');
state.matches.groupMilestones = {
list: [{ title: 'group-0.1' }],
totalCount: 1,
error: null,
};
mutations[types.RECEIVE_GROUP_MILESTONES_ERROR](state, error);
expect(state.matches.groupMilestones).toEqual({
list: [],
totalCount: 0,
error,
});
});
});
});
}); });
...@@ -27,6 +27,8 @@ describe('Release edit/new component', () => { ...@@ -27,6 +27,8 @@ describe('Release edit/new component', () => {
updateReleaseApiDocsPath: 'path/to/update/release/api/docs', updateReleaseApiDocsPath: 'path/to/update/release/api/docs',
releasesPagePath: 'path/to/releases/page', releasesPagePath: 'path/to/releases/page',
projectId: '8', projectId: '8',
groupId: '42',
groupMilestonesAvailable: true,
}; };
actions = { actions = {
......
...@@ -64,6 +64,8 @@ RSpec.describe ReleasesHelper do ...@@ -64,6 +64,8 @@ RSpec.describe ReleasesHelper do
describe '#data_for_edit_release_page' do describe '#data_for_edit_release_page' do
it 'has the needed data to display the "edit release" page' do it 'has the needed data to display the "edit release" page' do
keys = %i(project_id keys = %i(project_id
group_id
group_milestones_available
project_path project_path
tag_name tag_name
markdown_preview_path markdown_preview_path
...@@ -81,6 +83,8 @@ RSpec.describe ReleasesHelper do ...@@ -81,6 +83,8 @@ RSpec.describe ReleasesHelper do
describe '#data_for_new_release_page' do describe '#data_for_new_release_page' do
it 'has the needed data to display the "new release" page' do it 'has the needed data to display the "new release" page' do
keys = %i(project_id keys = %i(project_id
group_id
group_milestones_available
project_path project_path
releases_page_path releases_page_path
markdown_preview_path markdown_preview_path
......
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