Commit 66be6eb1 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch '13714-epics-select-vuex' into 'master'

Update EpicsSelect dropdown to use Vuex

Closes #13714

See merge request gitlab-org/gitlab-ee!15639
parents 3c23fb4a 107cc2a7
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import $ from 'jquery';
import { GlLoadingIcon } from '@gitlab/ui';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import { noneEpic } from 'ee/vue_shared/constants';
import EpicsSelectService from './service/epics_select_service';
import EpicsSelectStore from './store/epics_select_store';
import createStore from './store';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
......@@ -19,6 +18,7 @@ import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownContents from './dropdown_contents.vue';
export default {
store: createStore(),
components: {
GlLoadingIcon,
DropdownTitle,
......@@ -61,25 +61,14 @@ export default {
},
data() {
return {
service: new EpicsSelectService({
groupId: this.groupId,
}),
store: new EpicsSelectStore({
selectedEpic: this.initialEpic,
groupId: this.groupId,
selectedEpicIssueId: this.epicIssueId,
}),
showDropdown: false,
isEpicSelectLoading: false,
isEpicsLoading: false,
};
},
computed: {
epics() {
return this.store.getEpics();
},
selectedEpic() {
return this.store.getSelectedEpic();
...mapState(['epicSelectInProgress', 'epicsFetchInProgress', 'selectedEpic']),
...mapGetters(['groupEpics']),
dropdownSelectInProgress() {
return this.initialEpicLoading || this.epicSelectInProgress;
},
},
watch: {
......@@ -88,77 +77,28 @@ export default {
* So we need to watch for updates before updating local store.
*/
initialEpicLoading() {
this.store.setSelectedEpic(this.initialEpic);
this.setSelectedEpic(this.initialEpic);
},
},
mounted() {
this.setInitialData({
groupId: this.groupId,
issueId: this.issueId,
selectedEpic: this.selectedEpic,
selectedEpicIssueId: this.epicIssueId,
});
$(this.$refs.dropdown).on('shown.bs.dropdown', this.handleDropdownShown);
$(this.$refs.dropdown).on('hidden.bs.dropdown', this.handleDropdownHidden);
},
methods: {
fetchGroupEpics() {
this.isEpicsLoading = true;
return this.service
.getGroupEpics()
.then(({ data }) => {
this.isEpicsLoading = false;
this.store.setEpics(data);
})
.catch(() => {
this.isEpicsLoading = false;
createFlash(s__('Epics|Something went wrong while fetching group epics.'));
});
},
handleSelectSuccess({ data, epic, originalSelectedEpic }) {
// Verify if attachment was successful
this.isEpicSelectLoading = false;
if (data.epic.id === epic.id && data.issue.id === this.issueId) {
this.store.setSelectedEpicIssueId(data.id);
} else {
// Revert back to originally selected epic.
this.store.setSelectedEpic(originalSelectedEpic);
}
},
handleSelectFailure(errorMessage, originalSelectedEpic) {
this.isEpicSelectLoading = false;
// Revert back to originally selected epic in case of failure.
this.store.setSelectedEpic(originalSelectedEpic);
createFlash(errorMessage);
},
assignIssueToEpic(epic) {
const originalSelectedEpic = this.store.getSelectedEpic();
this.isEpicSelectLoading = true;
this.store.setSelectedEpic(epic);
return this.service
.assignIssueToEpic(this.issueId, epic)
.then(({ data }) => {
this.handleSelectSuccess({ data, epic, originalSelectedEpic });
})
.catch(() => {
this.handleSelectFailure(
s__('Epics|Something went wrong while assigning issue to epic.'),
originalSelectedEpic,
);
});
},
removeIssueFromEpic(epic) {
const originalSelectedEpic = this.store.getSelectedEpic();
this.isEpicSelectLoading = true;
this.store.setSelectedEpic(noneEpic);
return this.service
.removeIssueFromEpic(this.store.getSelectedEpicIssueId(), epic)
.then(({ data }) => {
this.handleSelectSuccess({ data, epic, originalSelectedEpic });
})
.catch(() => {
this.handleSelectFailure(
s__('Epics|Something went wrong while removing issue from epic.'),
originalSelectedEpic,
);
});
},
...mapActions([
'setInitialData',
'setSearchQuery',
'setSelectedEpic',
'fetchEpics',
'assignIssueToEpic',
'removeIssueFromEpic',
]),
handleEditClick() {
this.showDropdown = true;
......@@ -175,7 +115,7 @@ export default {
});
},
handleDropdownShown() {
if (this.epics.length === 0) this.fetchGroupEpics();
if (this.groupEpics.length === 0) this.fetchEpics();
},
handleDropdownHidden() {
this.showDropdown = false;
......@@ -187,9 +127,6 @@ export default {
this.assignIssueToEpic(epic);
}
},
handleSearchInput(query) {
this.store.filterEpics(query);
},
},
};
</script>
......@@ -200,7 +137,7 @@ export default {
<dropdown-title
:can-edit="canEdit"
:block-title="blockTitle"
:is-loading="initialEpicLoading || isEpicSelectLoading"
:is-loading="dropdownSelectInProgress"
@onClickEdit="handleEditClick"
/>
<dropdown-value v-show="!showDropdown" :epic="selectedEpic">
......@@ -214,14 +151,18 @@ export default {
dropdown-menu-epics dropdown-menu-selectable"
>
<dropdown-header />
<dropdown-search-input @onSearchInput="handleSearchInput" />
<dropdown-search-input @onSearchInput="setSearchQuery" />
<dropdown-contents
v-if="!isEpicsLoading"
:epics="epics"
v-if="!epicsFetchInProgress"
:epics="groupEpics"
:selected-epic="selectedEpic"
@onItemSelect="handleItemSelect"
/>
<gl-loading-icon v-if="isEpicsLoading" class="dropdown-contents-loading" size="md" />
<gl-loading-icon
v-if="epicsFetchInProgress"
class="dropdown-contents-loading"
size="md"
/>
</div>
</div>
</div>
......
import Api from 'ee/api';
export default class EpicsSelectService {
constructor({ groupId }) {
this.groupId = groupId;
}
getGroupEpics() {
return Api.groupEpics({
groupId: this.groupId,
});
}
// eslint-disable-next-line class-methods-use-this
assignIssueToEpic(issueId, epic) {
return Api.addEpicIssue({
issueId,
groupId: epic.groupId,
epicIid: epic.iid,
});
}
// eslint-disable-next-line class-methods-use-this
removeIssueFromEpic(epicIssueId, epic) {
return Api.removeEpicIssue({
epicIssueId,
groupId: epic.groupId,
epicIid: epic.iid,
});
}
}
import flash from '~/flash';
import { s__ } from '~/locale';
import Api from 'ee/api';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { noneEpic } from 'ee/vue_shared/constants';
import * as types from './mutation_types';
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const setSearchQuery = ({ commit }, searchQuery) =>
commit(types.SET_SEARCH_QUERY, searchQuery);
export const setSelectedEpic = ({ commit }, selectedEpic) =>
commit(types.SET_SELECTED_EPIC, selectedEpic);
export const requestEpics = ({ commit }) => commit(types.REQUEST_EPICS);
export const receiveEpicsSuccess = ({ state, commit }, data) => {
const epics = data
.filter(rawEpic => rawEpic.group_id === state.groupId)
.map(rawEpic =>
convertObjectPropsToCamelCase(Object.assign(rawEpic, { url: rawEpic.web_edit_url }), {
dropKeys: ['web_edit_url'],
}),
);
commit(types.RECEIVE_EPICS_SUCCESS, { epics });
};
export const receiveEpicsFailure = ({ commit }) => {
flash(s__('Epics|Something went wrong while fetching group epics.'));
commit(types.RECEIVE_EPICS_FAILURE);
};
export const fetchEpics = ({ state, dispatch }) => {
dispatch('requestEpics');
Api.groupEpics({
groupId: state.groupId,
})
.then(({ data }) => {
dispatch('receiveEpicsSuccess', data);
})
.catch(() => {
dispatch('receiveEpicsFailure');
});
};
export const requestIssueUpdate = ({ commit }) => commit(types.REQUEST_ISSUE_UPDATE);
export const receiveIssueUpdateSuccess = ({ state, commit }, { data, epic, isRemoval = false }) => {
// Verify if update was successful
if (data.epic.id === epic.id && data.issue.id === state.issueId) {
commit(types.RECEIVE_ISSUE_UPDATE_SUCCESS, {
selectedEpic: isRemoval ? noneEpic : epic,
selectedEpicIssueId: data.id,
});
}
};
/**
* Shows provided errorMessage in flash banner and
* fires `RECEIVE_ISSUE_UPDATE_FAILURE` mutation
*
* @param {string} errorMessage
*/
export const receiveIssueUpdateFailure = ({ commit }, errorMessage) => {
flash(errorMessage);
commit(types.RECEIVE_ISSUE_UPDATE_FAILURE);
};
export const assignIssueToEpic = ({ state, dispatch }, epic) => {
dispatch('requestIssueUpdate');
Api.addEpicIssue({
issueId: state.issueId,
groupId: epic.groupId,
epicIid: epic.iid,
})
.then(({ data }) => {
dispatch('receiveIssueUpdateSuccess', {
data,
epic,
});
})
.catch(() => {
// Shows flash error for Epic change failure
dispatch(
'receiveIssueUpdateFailure',
s__('Epics|Something went wrong while assigning issue to epic.'),
);
});
};
export const removeIssueFromEpic = ({ state, dispatch }, epic) => {
dispatch('requestIssueUpdate');
Api.removeEpicIssue({
epicIssueId: state.selectedEpicIssueId,
groupId: epic.groupId,
epicIid: epic.iid,
})
.then(({ data }) => {
dispatch('receiveIssueUpdateSuccess', {
data,
epic,
isRemoval: true,
});
})
.catch(() => {
// Shows flash error for Epic remove failure
dispatch(
'receiveIssueUpdateFailure',
s__('Epics|Something went wrong while removing issue from epic.'),
);
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import { convertObjectPropsToCamelCase, searchBy } from '~/lib/utils/common_utils';
export default class EpicsSelectStore {
constructor({ groupId, selectedEpic, selectedEpicIssueId }) {
this.groupId = groupId;
this.state = {};
this.state.epics = [];
this.state.allEpics = [];
this.state.selectedEpic = selectedEpic;
this.state.selectedEpicIssueId = selectedEpicIssueId;
}
setEpics(rawEpics) {
// Cache all Epics so that
// during search, we only work with `state.epics`
this.state.allEpics = rawEpics
.filter(epic => epic.group_id === this.groupId)
.map(epic =>
convertObjectPropsToCamelCase(Object.assign(epic, { url: epic.web_edit_url }), {
dropKeys: ['web_edit_url'],
}),
);
this.state.epics = this.state.allEpics;
}
getEpics() {
return this.state.epics;
}
filterEpics(query) {
if (query) {
this.state.epics = this.state.allEpics.filter(epic => {
const { title, reference, url, iid } = epic;
// In case user has just pasted ID
// We need to be specific with the search
if (Number(query)) {
return query.includes(iid);
}
return searchBy(query, {
title,
reference,
url,
});
});
} else {
this.state.epics = this.state.allEpics;
}
}
setSelectedEpic(selectedEpic) {
this.state.selectedEpic = selectedEpic;
}
setSelectedEpicIssueId(selectedEpicIssueId) {
this.state.selectedEpicIssueId = selectedEpicIssueId;
}
getSelectedEpic() {
return this.state.selectedEpic;
}
getSelectedEpicIssueId() {
return this.state.selectedEpicIssueId;
}
}
import { searchBy } from '~/lib/utils/common_utils';
/**
* Returns array of Epics
*
* 1. When state.searchQuery is empty, all Epics are returned.
* 2. When state.searchQuery has value, Epics list is filtered
* using the searchQuery against `iid`, `title`, `reference`
* and `url` props of Epic object.
*
* @param {object} state
*/
export const groupEpics = state => {
if (state.searchQuery) {
return state.epics.filter(epic => {
const { title, reference, url, iid } = epic;
// In case user has just pasted ID
// We need to be specific with the search
if (Number(state.searchQuery)) {
return state.searchQuery.includes(iid);
}
return searchBy(state.searchQuery, {
title,
reference,
url,
});
});
}
return state.epics;
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import getDefaultState from './state';
Vue.use(Vuex);
const createStore = () =>
new Vuex.Store({
state: getDefaultState(),
actions,
getters,
mutations,
});
export default createStore;
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
export const SET_SELECTED_EPIC = 'SET_SELECTED_EPIC';
export const REQUEST_EPICS = 'REQUEST_EPICS';
export const RECEIVE_EPICS_SUCCESS = 'RECEIVE_EPICS_SUCCESS';
export const RECEIVE_EPICS_FAILURE = 'RECEIVE_EPICS_FAILURE';
export const REQUEST_ISSUE_UPDATE = 'REQUEST_ISSUE_UPDATE';
export const RECEIVE_ISSUE_UPDATE_SUCCESS = 'RECEIVE_ISSUE_UPDATE_SUCCESS';
export const RECEIVE_ISSUE_UPDATE_FAILURE = 'RECEIVE_ISSUE_UPDATE_FAILURE';
import * as types from './mutation_types';
export default {
[types.SET_INITIAL_DATA](state, { groupId, issueId, selectedEpic, selectedEpicIssueId }) {
state.groupId = groupId;
state.issueId = issueId;
state.selectedEpic = selectedEpic;
state.selectedEpicIssueId = selectedEpicIssueId;
},
[types.SET_SEARCH_QUERY](state, searchQuery) {
state.searchQuery = searchQuery;
},
[types.SET_SELECTED_EPIC](state, selectedEpic) {
state.selectedEpic = selectedEpic;
},
[types.REQUEST_EPICS](state) {
state.epicsFetchInProgress = true;
},
[types.RECEIVE_EPICS_SUCCESS](state, { epics }) {
state.epicsFetchInProgress = false;
state.epics = epics;
},
[types.RECEIVE_EPICS_FAILURE](state) {
state.epicsFetchInProgress = false;
},
[types.REQUEST_ISSUE_UPDATE](state) {
state.epicSelectInProgress = true;
},
[types.RECEIVE_ISSUE_UPDATE_SUCCESS](state, { selectedEpic, selectedEpicIssueId }) {
state.epicSelectInProgress = false;
state.selectedEpic = selectedEpic;
state.selectedEpicIssueId = selectedEpicIssueId;
},
[types.RECEIVE_ISSUE_UPDATE_FAILURE](state) {
state.epicSelectInProgress = false;
},
};
export default () => ({
// Initial Data
groupId: null,
issueId: null,
selectedEpic: {},
selectedEpicIssueId: null,
// Store
searchQuery: '',
epics: [],
// UI Flags
epicSelectInProgress: false,
epicsFetchInProgress: false,
});
......@@ -2,7 +2,11 @@ import { shallowMount } from '@vue/test-utils';
import SidebarItemEpicsSelect from 'ee/sidebar/components/sidebar_item_epics_select.vue';
import { mockSidebarStore, mockEpic1, mockIssue } from '../mock_data';
import {
mockSidebarStore,
mockEpic1,
mockIssue,
} from '../../vue_shared/components/sidebar/mock_data';
describe('SidebarItemEpicsSelect', () => {
let wrapper;
......
import { shallowMount } from '@vue/test-utils';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import EpicsSelectBase from 'ee/vue_shared/components/sidebar/epics_select/base.vue';
......@@ -11,26 +11,21 @@ import DropdownHeader from 'ee/vue_shared/components/sidebar/epics_select/dropdo
import DropdownSearchInput from 'ee/vue_shared/components/sidebar/epics_select/dropdown_search_input.vue';
import DropdownContents from 'ee/vue_shared/components/sidebar/epics_select/dropdown_contents.vue';
import EpicsSelectService from 'ee/vue_shared/components/sidebar/epics_select/service/epics_select_service';
import EpicsSelectStore from 'ee/vue_shared/components/sidebar/epics_select/store/epics_select_store';
import createDefaultStore from 'ee/vue_shared/components/sidebar/epics_select/store';
import {
mockEpic1,
mockEpic2,
mockIssue,
mockEpics,
mockAssignRemoveRes,
noneEpic,
} from '../../../../sidebar/mock_data';
import { mockEpic1, mockEpic2, mockIssue, noneEpic } from '../mock_data';
describe('EpicsSelect', () => {
describe('Base', () => {
const errorMessage = 'Something went wrong while fetching group epics.';
let wrapper;
// const errorMessage = 'Something went wrong while fetching group epics.';
const store = createDefaultStore();
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
wrapper = shallowMount(EpicsSelectBase, {
store,
localVue: createLocalVue(),
propsData: {
canEdit: true,
blockTitle: 'Epic',
......@@ -47,14 +42,8 @@ describe('EpicsSelect', () => {
wrapper.destroy();
});
describe('data', () => {
it('should have `service` & `store` props initialized', () => {
expect(wrapper.vm.service instanceof EpicsSelectService).toBe(true);
expect(wrapper.vm.store instanceof EpicsSelectStore).toBe(true);
});
});
describe('methods', () => {
/*
describe('fetchGroupEpics', () => {
it('should call `service.getGroupEpics` and set response to store on request success', done => {
jest.spyOn(wrapper.vm.service, 'getGroupEpics').mockResolvedValue({ data: mockEpics });
......@@ -312,17 +301,18 @@ describe('EpicsSelect', () => {
.catch(done.fail);
});
});
*/
describe('handleDropdownShown', () => {
it('should call `fetchGroupEpics` when store does not have any epics loaded yet', done => {
jest.spyOn(wrapper.vm, 'fetchGroupEpics');
it('should call `fetchEpics` when `groupEpics` does not return any epics', done => {
jest.spyOn(wrapper.vm, 'fetchEpics');
wrapper.vm.store.setEpics([]);
store.dispatch('receiveEpicsSuccess', []);
wrapper.vm.$nextTick(() => {
wrapper.vm.handleDropdownShown();
expect(wrapper.vm.fetchGroupEpics).toHaveBeenCalled();
expect(wrapper.vm.fetchEpics).toHaveBeenCalled();
done();
});
......@@ -340,6 +330,7 @@ describe('EpicsSelect', () => {
describe('handleItemSelect', () => {
it('should call `removeIssueFromEpic` with selected epic when `epic` param represents `No Epic`', () => {
jest.spyOn(wrapper.vm, 'removeIssueFromEpic');
store.dispatch('setSelectedEpic', mockEpic1);
wrapper.vm.handleItemSelect(noneEpic);
......@@ -354,16 +345,6 @@ describe('EpicsSelect', () => {
expect(wrapper.vm.assignIssueToEpic).toHaveBeenCalledWith(mockEpic2);
});
});
describe('handleSearchInput', () => {
it('should call `store.filterEpics` with passed `query` param', () => {
jest.spyOn(wrapper.vm.store, 'filterEpics');
wrapper.vm.handleSearchInput('foo');
expect(wrapper.vm.store.filterEpics).toHaveBeenCalledWith('foo');
});
});
});
describe('template', () => {
......@@ -445,9 +426,7 @@ describe('EpicsSelect', () => {
it('should render DropdownContents component when props `canEdit` & `showDropdown` are true and `isEpicsLoading` is false', done => {
showDropdown();
wrapper.setData({
isEpicsLoading: false,
});
store.dispatch('receiveEpicsSuccess', []);
wrapper.vm.$nextTick(() => {
expect(wrapper.find(DropdownContents).exists()).toBe(true);
......@@ -457,9 +436,7 @@ describe('EpicsSelect', () => {
it('should render GlLoadingIcon component when props `canEdit` & `showDropdown` and `isEpicsLoading` are true', done => {
showDropdown();
wrapper.setData({
isEpicsLoading: true,
});
store.dispatch('requestEpics');
wrapper.vm.$nextTick(() => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
......
......@@ -4,7 +4,7 @@ import { GlLink } from '@gitlab/ui';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DropdownContents from 'ee/vue_shared/components/sidebar/epics_select/dropdown_contents.vue';
import { mockEpic1, mockEpic2, mockEpics, noneEpic } from '../../../../sidebar/mock_data';
import { mockEpic1, mockEpic2, mockEpics, noneEpic } from '../mock_data';
const epics = mockEpics.map(epic => convertObjectPropsToCamelCase(epic));
......
......@@ -4,7 +4,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import DropdownValueCollapsed from 'ee/vue_shared/components/sidebar/epics_select/dropdown_value_collapsed.vue';
import { mockEpic1 } from '../../../../sidebar/mock_data';
import { mockEpic1 } from '../mock_data';
describe('EpicsSelect', () => {
describe('DropdownValueCollapsed', () => {
......
......@@ -4,7 +4,7 @@ import { GlLink } from '@gitlab/ui';
import DropdownValue from 'ee/vue_shared/components/sidebar/epics_select/dropdown_value.vue';
import { mockEpic1 } from '../../../../sidebar/mock_data';
import { mockEpic1 } from '../mock_data';
describe('EpicsSelect', () => {
describe('DropdownValue', () => {
......
import Api from 'ee/api';
import EpicsSelectService from 'ee/vue_shared/components/sidebar/epics_select/service/epics_select_service';
import {
mockEpic1,
mockIssue,
mockEpics,
mockAssignRemoveRes,
} from '../../../../../sidebar/mock_data';
describe('EpicsSelect', () => {
describe('Service', () => {
const service = new EpicsSelectService({ groupId: mockEpic1.group_id });
describe('getGroupEpics', () => {
it('calls `Api.groupEpics` with `groupId`', () => {
jest.spyOn(Api, 'groupEpics').mockResolvedValue({ data: mockEpics });
service.getGroupEpics();
expect(Api.groupEpics).toHaveBeenCalledWith(
expect.objectContaining({
groupId: mockEpic1.group_id,
}),
);
});
});
describe('assignIssueToEpic', () => {
it('calls `Api.addEpicIssue` with `issueId`, `groupId` & `epicIid`', () => {
jest.spyOn(Api, 'addEpicIssue').mockResolvedValue({ data: mockAssignRemoveRes });
service.assignIssueToEpic(mockIssue.id, {
groupId: mockEpic1.group_id,
iid: mockEpic1.iid,
});
expect(Api.addEpicIssue).toHaveBeenCalledWith(
expect.objectContaining({
issueId: mockIssue.id,
groupId: mockEpic1.group_id,
epicIid: mockEpic1.iid,
}),
);
});
});
describe('removeIssueFromEpic', () => {
it('calls `Api.removeEpicIssue` with `epicIssueId`, `groupId` & `epicIid`', () => {
jest.spyOn(Api, 'removeEpicIssue').mockResolvedValue({ data: mockAssignRemoveRes });
service.removeIssueFromEpic(mockIssue.epic_issue_id, {
groupId: mockEpic1.group_id,
iid: mockEpic1.iid,
});
expect(Api.removeEpicIssue).toHaveBeenCalledWith(
expect.objectContaining({
epicIssueId: mockIssue.epic_issue_id,
groupId: mockEpic1.group_id,
epicIid: mockEpic1.iid,
}),
);
});
});
});
});
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import EpicsSelectStore from 'ee/vue_shared/components/sidebar/epics_select/store/epics_select_store';
import { mockIssue, mockEpics } from '../../../../../sidebar/mock_data';
describe('EpicsSelect', () => {
describe('Store', () => {
const normalizedEpics = mockEpics.map(epic =>
convertObjectPropsToCamelCase(Object.assign(epic, { url: epic.web_edit_url }), {
dropKeys: ['web_edit_url'],
}),
);
let store;
beforeEach(() => {
store = new EpicsSelectStore({
groupId: normalizedEpics[0].groupId,
selectedEpic: normalizedEpics[0],
selectedEpicIssueId: mockIssue.epic_issue_id,
});
});
describe('constructor', () => {
it('should initialize `state` with all the required properties', () => {
expect(store.groupId).toBe(normalizedEpics[0].groupId);
expect(store.state).toEqual(
expect.objectContaining({
epics: [],
allEpics: [],
selectedEpic: normalizedEpics[0],
selectedEpicIssueId: mockIssue.epic_issue_id,
}),
);
});
});
describe('setEpics', () => {
it('should set passed `rawEpics` into the store state by normalizing it', () => {
store.setEpics(mockEpics);
expect(store.state.epics.length).toBe(mockEpics.length);
expect(store.state.allEpics.length).toBe(mockEpics.length);
expect(store.state.epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[0],
}),
);
expect(store.state.allEpics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[0],
}),
);
});
});
describe('getEpics', () => {
it('should return value of `state.epics`', () => {
store.setEpics(mockEpics);
const epics = store.getEpics();
expect(epics.length).toBe(mockEpics.length);
});
});
describe('filterEpics', () => {
beforeEach(() => {
store.setEpics(mockEpics);
});
it('should return `state.epics` filtered Epic Title', () => {
store.filterEpics('consequatur');
const epics = store.getEpics();
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[0],
}),
);
});
it('should return `state.epics` filtered Epic Reference', () => {
store.filterEpics('gitlab-org&1');
const epics = store.getEpics();
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[0],
}),
);
});
it('should return `state.epics` filtered Epic URL', () => {
store.filterEpics('http://gitlab.example.com/groups/gitlab-org/-/epics/2');
const epics = store.getEpics();
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[1],
}),
);
});
it('should return `state.epics` filtered Epic Iid', () => {
store.filterEpics('2');
const epics = store.getEpics();
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[1],
}),
);
});
it('should return `state.epics` without any filters when query is empty', () => {
store.filterEpics('');
const epics = store.getEpics();
expect(epics.length).toBe(normalizedEpics.length);
epics.forEach((epic, index) => {
expect.objectContaining({
...normalizedEpics[index],
});
});
});
});
describe('setSelectedEpic', () => {
it('should set provided `selectedEpic` param to store state', () => {
store.setSelectedEpic(normalizedEpics[1]);
expect(store.state.selectedEpic).toBe(normalizedEpics[1]);
});
});
describe('setSelectedEpicIssueId', () => {
it('should set provided `selectedEpicIssueId` param to store state', () => {
store.setSelectedEpicIssueId(7);
expect(store.state.selectedEpicIssueId).toBe(7);
});
});
describe('getSelectedEpic', () => {
it('should return value of `selectedEpic` from store state', () => {
store.setSelectedEpic(normalizedEpics[1]);
expect(store.getSelectedEpic()).toBe(normalizedEpics[1]);
});
});
describe('getSelectedEpicIssueId', () => {
it('should return value of `selectedEpicIssueId` from store state', () => {
store.setSelectedEpicIssueId(7);
expect(store.getSelectedEpicIssueId()).toBe(7);
});
});
});
});
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import * as getters from 'ee/vue_shared/components/sidebar/epics_select/store/getters';
import createDefaultState from 'ee/vue_shared/components/sidebar/epics_select/store/state';
import { mockEpics } from '../../mock_data';
describe('EpicsSelect', () => {
describe('store', () => {
describe('getters', () => {
let state;
const normalizedEpics = mockEpics.map(rawEpic =>
convertObjectPropsToCamelCase(Object.assign(rawEpic, { url: rawEpic.web_edit_url }), {
dropKeys: ['web_edit_url'],
}),
);
beforeEach(() => {
state = createDefaultState();
state.epics = normalizedEpics;
});
describe('groupEpics', () => {
it('should return `state.epics` without any filters when `state.searchQuery` is empty', () => {
state.searchQuery = '';
const epics = getters.groupEpics(state);
expect(epics.length).toBe(normalizedEpics.length);
epics.forEach((epic, index) => {
expect.objectContaining({
...normalizedEpics[index],
});
});
});
it('should return `state.epics` filtered by Epic Title', () => {
state.searchQuery = 'consequatur';
const epics = getters.groupEpics(state);
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[0],
}),
);
});
it('should return `state.epics` filtered by Epic Reference', () => {
state.searchQuery = 'gitlab-org&1';
const epics = getters.groupEpics(state);
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[0],
}),
);
});
it('should return `state.epics` filtered Epic URL', () => {
state.searchQuery = 'http://gitlab.example.com/groups/gitlab-org/-/epics/2';
const epics = getters.groupEpics(state);
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[1],
}),
);
});
it('should return `state.epics` filtered by Epic Iid', () => {
state.searchQuery = '2';
const epics = getters.groupEpics(state);
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[1],
}),
);
});
});
});
});
});
import mutations from 'ee/vue_shared/components/sidebar/epics_select/store/mutations';
import createDefaultState from 'ee/vue_shared/components/sidebar/epics_select/store/state';
import * as types from 'ee/vue_shared/components/sidebar/epics_select/store/mutation_types';
import { mockEpic1, mockIssue } from '../../mock_data';
describe('EpicsSelect', () => {
describe('store', () => {
describe('mutations', () => {
let state;
beforeEach(() => {
state = createDefaultState();
});
describe(types.SET_INITIAL_DATA, () => {
it('should set provided `data` param props to state', () => {
const data = {
groupId: mockEpic1.group_id,
issueId: mockIssue.id,
selectedEpic: mockEpic1,
selectedEpicIssueId: mockIssue.epic_issue_id,
};
mutations[types.SET_INITIAL_DATA](state, data);
expect(state).toHaveProperty('groupId', data.groupId);
expect(state).toHaveProperty('issueId', data.issueId);
expect(state).toHaveProperty('selectedEpic', data.selectedEpic);
expect(state).toHaveProperty('selectedEpicIssueId', data.selectedEpicIssueId);
});
});
describe(types.SET_SEARCH_QUERY, () => {
it('should set `searchQuery` param to state', () => {
const searchQuery = 'foo';
mutations[types.SET_SEARCH_QUERY](state, searchQuery);
expect(state).toHaveProperty('searchQuery', searchQuery);
});
});
describe(types.SET_SELECTED_EPIC, () => {
it('should set `selectedEpic` param to state', () => {
mutations[types.SET_SELECTED_EPIC](state, mockEpic1);
expect(state).toHaveProperty('selectedEpic', mockEpic1);
});
});
describe(types.REQUEST_EPICS, () => {
it('should set `state.epicsFetchInProgress` to true', () => {
mutations[types.REQUEST_EPICS](state);
expect(state.epicsFetchInProgress).toBe(true);
});
});
describe(types.RECEIVE_EPICS_SUCCESS, () => {
it('should set `state.epicsFetchInProgress` to false `epics` param to state', () => {
mutations[types.RECEIVE_EPICS_SUCCESS](state, { epics: [mockEpic1] });
expect(state.epicsFetchInProgress).toBe(false);
expect(state.epics).toEqual(expect.arrayContaining([mockEpic1]));
});
});
describe(types.RECEIVE_EPICS_FAILURE, () => {
it('should set `state.epicsFetchInProgress` to false', () => {
mutations[types.RECEIVE_EPICS_FAILURE](state);
expect(state.epicsFetchInProgress).toBe(false);
});
});
describe(types.REQUEST_ISSUE_UPDATE, () => {
it('should set `state.epicSelectInProgress` to true', () => {
mutations[types.REQUEST_ISSUE_UPDATE](state);
expect(state.epicSelectInProgress).toBe(true);
});
});
describe(types.RECEIVE_ISSUE_UPDATE_SUCCESS, () => {
it('should set `state.epicSelectInProgress` to false and `selectedEpic` & `selectedEpicIssueId` params to state', () => {
mutations[types.RECEIVE_ISSUE_UPDATE_SUCCESS](state, {
selectedEpic: mockEpic1,
selectedEpicIssueId: mockIssue.epic_issue_id,
});
expect(state.epicSelectInProgress).toBe(false);
expect(state.selectedEpic).toBe(mockEpic1);
expect(state.selectedEpicIssueId).toBe(mockIssue.epic_issue_id);
});
});
describe(types.RECEIVE_ISSUE_UPDATE_FAILURE, () => {
it('should set `state.epicSelectInProgress` to false', () => {
mutations[types.RECEIVE_ISSUE_UPDATE_FAILURE](state);
expect(state.epicSelectInProgress).toBe(false);
});
});
});
});
});
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