Commit 0f1517b6 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'roadmap-filter-by-epic' into 'master'

Filter by epic in roadmap

See merge request gitlab-org/gitlab!58642
parents b17c41f4 dedbe7ba
<script>
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { debounce } from 'lodash';
import createFlash from '~/flash';
import { isNumeric } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import { DEBOUNCE_DELAY } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
export default {
components: {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlLoadingIcon,
},
props: {
config: {
type: Object,
required: true,
},
value: {
type: Object,
required: true,
},
},
data() {
return {
epics: this.config.initialEpics || [],
loading: true,
};
},
computed: {
currentValue() {
/*
* When the URL contains the epic_iid, we'd get: '123'
*/
if (isNumeric(this.value.data)) {
return parseInt(this.value.data, 10);
}
/*
* When the token is added in current session it'd be: 'Foo::&123'
*/
const id = this.value.data.split('::&')[1];
if (id) {
return parseInt(id, 10);
}
return this.value.data;
},
activeEpic() {
const currentValueIsString = typeof this.currentValue === 'string';
return this.epics.find(
(epic) => epic[currentValueIsString ? 'title' : 'iid'] === this.currentValue,
);
},
},
watch: {
active: {
immediate: true,
handler(newValue) {
if (!newValue && !this.epics.length) {
this.searchEpics({ data: this.currentValue });
}
},
},
},
methods: {
fetchEpicsBySearchTerm(searchTerm = '') {
this.loading = true;
this.config
.fetchEpics(searchTerm)
.then(({ data }) => {
this.epics = data;
})
.catch(() => createFlash({ message: __('There was a problem fetching epics.') }))
.finally(() => {
this.loading = false;
});
},
fetchSingleEpic(iid) {
this.loading = true;
this.config
.fetchSingleEpic(iid)
.then(({ data }) => {
this.epics = [data];
})
.catch(() => createFlash({ message: __('There was a problem fetching epics.') }))
.finally(() => {
this.loading = false;
});
},
searchEpics: debounce(function debouncedSearch({ data }) {
if (isNumeric(data)) {
return this.fetchSingleEpic(data);
}
return this.fetchEpicsBySearchTerm(data);
}, DEBOUNCE_DELAY),
getEpicValue(epic) {
return `${epic.title}::&${epic.iid}`;
},
},
stripQuotes,
};
</script>
<template>
<gl-filtered-search-token
:config="config"
v-bind="{ ...$props, ...$attrs }"
v-on="$listeners"
@input="searchEpics"
>
<template #view="{ inputValue }">
<span>{{ activeEpic ? getEpicValue(activeEpic) : $options.stripQuotes(inputValue) }}</span>
</template>
<template #suggestions>
<gl-loading-icon v-if="loading" />
<template v-else>
<gl-filtered-search-suggestion
v-for="epic in epics"
:key="epic.id"
:value="getEpicValue(epic)"
>
<div>{{ epic.title }}</div>
</gl-filtered-search-suggestion>
</template>
</template>
</gl-filtered-search-token>
</template>
...@@ -42,6 +42,7 @@ toggle the list of the milestone bars. ...@@ -42,6 +42,7 @@ toggle the list of the milestone bars.
> - Filtering roadmaps by milestone is recommended for production use. > - Filtering roadmaps by milestone is recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-filtering-roadmaps-by-milestone). **(PREMIUM SELF)** > - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-filtering-roadmaps-by-milestone). **(PREMIUM SELF)**
> - Filtering by epic confidentiality [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218624) in GitLab 13.9. > - Filtering by epic confidentiality [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218624) in GitLab 13.9.
> - Filtering by epic [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218623) in GitLab 13.11.
WARNING: WARNING:
Filtering roadmaps by milestone might not be available to you. Check the **version history** note above for details. Filtering roadmaps by milestone might not be available to you. Check the **version history** note above for details.
...@@ -69,8 +70,10 @@ You can also filter epics in the Roadmap view by the epics': ...@@ -69,8 +70,10 @@ You can also filter epics in the Roadmap view by the epics':
- Label - Label
- Milestone - Milestone
- Confidentiality - Confidentiality
- Epic
- Your Reaction
![roadmap date range in weeks](img/roadmap_filters_v13_8.png) ![roadmap date range in weeks](img/roadmap_filters_v13_11.png)
Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap-in-epics). Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap-in-epics).
......
...@@ -3,7 +3,11 @@ import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui'; ...@@ -3,7 +3,11 @@ import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
import { dateInWords } from '~/lib/utils/datetime_utility'; import { dateInWords } from '~/lib/utils/datetime_utility';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { emptyStateDefault, emptyStateWithFilters } from '../constants'; import {
emptyStateDefault,
emptyStateWithFilters,
emptyStateWithEpicIidFiltered,
} from '../constants';
import CommonMixin from '../mixins/common_mixin'; import CommonMixin from '../mixins/common_mixin';
export default { export default {
...@@ -41,6 +45,11 @@ export default { ...@@ -41,6 +45,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
filterParams: {
type: Object,
required: false,
default: () => ({}),
},
}, },
computed: { computed: {
timeframeRange() { timeframeRange() {
...@@ -100,6 +109,10 @@ export default { ...@@ -100,6 +109,10 @@ export default {
); );
} }
if (this.hasFiltersApplied && Boolean(this.filterParams?.epicIid)) {
return emptyStateWithEpicIidFiltered;
}
if (this.hasFiltersApplied) { if (this.hasFiltersApplied) {
return sprintf(emptyStateWithFilters, { return sprintf(emptyStateWithFilters, {
startDate: this.timeframeRange.startDate, startDate: this.timeframeRange.startDate,
......
...@@ -60,6 +60,7 @@ export default { ...@@ -60,6 +60,7 @@ export default {
'isChildEpics', 'isChildEpics',
'hasFiltersApplied', 'hasFiltersApplied',
'milestonesFetchFailure', 'milestonesFetchFailure',
'filterParams',
]), ]),
showFilteredSearchbar() { showFilteredSearchbar() {
if (this.glFeatures.asyncFiltering) { if (this.glFeatures.asyncFiltering) {
...@@ -176,6 +177,7 @@ export default { ...@@ -176,6 +177,7 @@ export default {
:has-filters-applied="hasFiltersApplied" :has-filters-applied="hasFiltersApplied"
:empty-state-illustration-path="emptyStateIllustrationPath" :empty-state-illustration-path="emptyStateIllustrationPath"
:is-child-epics="isChildEpics" :is-child-epics="isChildEpics"
:filter-params="filterParams"
/> />
<roadmap-shell <roadmap-shell
v-else-if="!epicsFetchFailure" v-else-if="!epicsFetchFailure"
......
...@@ -48,6 +48,10 @@ export const emptyStateWithFilters = s__( ...@@ -48,6 +48,10 @@ export const emptyStateWithFilters = s__(
'GroupRoadmap|To widen your search, change or remove filters; from %{startDate} to %{endDate}.', 'GroupRoadmap|To widen your search, change or remove filters; from %{startDate} to %{endDate}.',
); );
export const emptyStateWithEpicIidFiltered = s__(
'GroupRoadmap|To make your epics appear in the roadmap, add start or due dates to them.',
);
export const PRESET_DEFAULTS = { export const PRESET_DEFAULTS = {
QUARTERS: { QUARTERS: {
TIMEFRAME_LENGTH: 21, TIMEFRAME_LENGTH: 21,
......
...@@ -6,18 +6,25 @@ import { __ } from '~/locale'; ...@@ -6,18 +6,25 @@ import { __ } from '~/locale';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import { FilterTokenOperators } from '../constants'; import { FilterTokenOperators } from '../constants';
export default { export default {
inject: ['groupFullPath', 'groupMilestonesPath'], inject: ['groupFullPath', 'groupMilestonesPath', 'listEpicsPath'],
computed: { computed: {
urlParams() { urlParams() {
const { search, authorUsername, labelName, milestoneTitle, confidential, myReactionEmoji } = const {
this.filterParams || {}; search,
authorUsername,
labelName,
milestoneTitle,
confidential,
myReactionEmoji,
epicIid,
} = this.filterParams || {};
return { return {
state: this.currentState || this.epicsState, state: this.currentState || this.epicsState,
page: this.currentPage, page: this.currentPage,
...@@ -29,6 +36,7 @@ export default { ...@@ -29,6 +36,7 @@ export default {
milestone_title: milestoneTitle, milestone_title: milestoneTitle,
confidential, confidential,
my_reaction_emoji: myReactionEmoji, my_reaction_emoji: myReactionEmoji,
epic_iid: epicIid && Number(epicIid),
search, search,
}; };
}, },
...@@ -104,6 +112,23 @@ export default { ...@@ -104,6 +112,23 @@ export default {
{ icon: 'eye', value: false, title: __('No') }, { icon: 'eye', value: false, title: __('No') },
], ],
}, },
{
type: 'epic_iid',
icon: 'epic',
title: __('Epic'),
unique: true,
symbol: '&',
token: EpicToken,
operators: FilterTokenOperators,
fetchEpics: (search = '') => {
return axios.get(this.listEpicsPath, { params: { search } }).then(({ data }) => {
return { data };
});
},
fetchSingleEpic: (iid) => {
return axios.get(`${this.listEpicsPath}/${iid}`).then(({ data }) => ({ data }));
},
},
]; ];
if (gon.current_user_id) { if (gon.current_user_id) {
...@@ -133,8 +158,15 @@ export default { ...@@ -133,8 +158,15 @@ export default {
return tokens; return tokens;
}, },
getFilteredSearchValue() { getFilteredSearchValue() {
const { authorUsername, labelName, milestoneTitle, confidential, myReactionEmoji, search } = const {
this.filterParams || {}; authorUsername,
labelName,
milestoneTitle,
confidential,
myReactionEmoji,
search,
epicIid,
} = this.filterParams || {};
const filteredSearchValue = []; const filteredSearchValue = [];
if (authorUsername) { if (authorUsername) {
...@@ -174,6 +206,13 @@ export default { ...@@ -174,6 +206,13 @@ export default {
}); });
} }
if (epicIid) {
filteredSearchValue.push({
type: 'epic_iid',
value: { data: epicIid },
});
}
if (search) { if (search) {
filteredSearchValue.push(search); filteredSearchValue.push(search);
} }
...@@ -202,6 +241,9 @@ export default { ...@@ -202,6 +241,9 @@ export default {
case 'my_reaction_emoji': case 'my_reaction_emoji':
filterParams.myReactionEmoji = filter.value.data; filterParams.myReactionEmoji = filter.value.data;
break; break;
case 'epic_iid':
filterParams.epicIid = Number(filter.value.data.split('::&')[1]);
break;
case 'filtered-search-term': case 'filtered-search-term':
if (filter.value.data) plainText.push(filter.value.data); if (filter.value.data) plainText.push(filter.value.data);
break; break;
......
...@@ -4,6 +4,7 @@ query groupEpics( ...@@ -4,6 +4,7 @@ query groupEpics(
$fullPath: ID! $fullPath: ID!
$state: EpicState $state: EpicState
$sort: EpicSort $sort: EpicSort
$iid: ID
$startDate: Time $startDate: Time
$dueDate: Time $dueDate: Time
$labelName: [String!] = [] $labelName: [String!] = []
...@@ -18,6 +19,7 @@ query groupEpics( ...@@ -18,6 +19,7 @@ query groupEpics(
id id
name name
epics( epics(
iid: $iid
state: $state state: $state
sort: $sort sort: $sort
startDate: $startDate startDate: $startDate
......
...@@ -77,6 +77,10 @@ export default () => { ...@@ -77,6 +77,10 @@ export default () => {
...(rawFilterParams.confidential && { ...(rawFilterParams.confidential && {
confidential: parseBoolean(rawFilterParams.confidential), confidential: parseBoolean(rawFilterParams.confidential),
}), }),
...(rawFilterParams.epicIid && {
epicIid: parseInt(rawFilterParams.epicIid, 10),
}),
}; };
const timeframe = getTimeframeForPreset( const timeframe = getTimeframeForPreset(
presetType, presetType,
......
...@@ -45,6 +45,10 @@ const fetchGroupEpics = ( ...@@ -45,6 +45,10 @@ const fetchGroupEpics = (
...filterParams, ...filterParams,
first: gon.roadmap_epics_limit + 1, first: gon.roadmap_epics_limit + 1,
}; };
if (filterParams?.epicIid) {
variables.iid = filterParams.epicIid;
}
} }
return epicUtils.gqClient return epicUtils.gqClient
......
---
title: Filter by epic in roadmap
merge_request: 58642
author:
type: added
...@@ -54,6 +54,7 @@ const mockProvide = { ...@@ -54,6 +54,7 @@ const mockProvide = {
groupFullPath: 'gitlab-org', groupFullPath: 'gitlab-org',
groupLabelsPath: '/gitlab-org/-/labels.json', groupLabelsPath: '/gitlab-org/-/labels.json',
groupMilestonesPath: '/gitlab-org/-/milestone.json', groupMilestonesPath: '/gitlab-org/-/milestone.json',
listEpicsPath: '/gitlab-org/-/epics',
emptyStatePath: '/assets/illustrations/empty-state/epics.svg', emptyStatePath: '/assets/illustrations/empty-state/epics.svg',
}; };
......
...@@ -47,6 +47,7 @@ describe('RoadmapApp', () => { ...@@ -47,6 +47,7 @@ describe('RoadmapApp', () => {
glFeatures: { asyncFiltering: true }, glFeatures: { asyncFiltering: true },
groupFullPath: 'gitlab-org', groupFullPath: 'gitlab-org',
groupMilestonesPath: '/groups/gitlab-org/-/milestones.json', groupMilestonesPath: '/groups/gitlab-org/-/milestones.json',
listEpicsPath: '/groups/gitlab-org/-/epics',
}, },
store, store,
}); });
......
...@@ -13,6 +13,7 @@ import { visitUrl, mergeUrlParams, updateHistory } from '~/lib/utils/url_utility ...@@ -13,6 +13,7 @@ import { visitUrl, mergeUrlParams, updateHistory } from '~/lib/utils/url_utility
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
...@@ -28,6 +29,7 @@ const createComponent = ({ ...@@ -28,6 +29,7 @@ const createComponent = ({
epicsState = EPICS_STATES.ALL, epicsState = EPICS_STATES.ALL,
sortedBy = mockSortedBy, sortedBy = mockSortedBy,
groupFullPath = 'gitlab-org', groupFullPath = 'gitlab-org',
listEpicsPath = '/groups/gitlab-org/-/epics',
groupMilestonesPath = '/groups/gitlab-org/-/milestones.json', groupMilestonesPath = '/groups/gitlab-org/-/milestones.json',
timeframe = getTimeframeForMonthsView(mockTimeframeInitialDate), timeframe = getTimeframeForMonthsView(mockTimeframeInitialDate),
filterParams = {}, filterParams = {},
...@@ -51,6 +53,7 @@ const createComponent = ({ ...@@ -51,6 +53,7 @@ const createComponent = ({
provide: { provide: {
groupFullPath, groupFullPath,
groupMilestonesPath, groupMilestonesPath,
listEpicsPath,
}, },
}); });
}; };
...@@ -205,6 +208,17 @@ describe('RoadmapFilters', () => { ...@@ -205,6 +208,17 @@ describe('RoadmapFilters', () => {
{ icon: 'eye', value: false, title: 'No' }, { icon: 'eye', value: false, title: 'No' },
], ],
}, },
{
type: 'epic_iid',
icon: 'epic',
title: 'Epic',
unique: true,
symbol: '&',
token: EpicToken,
operators,
fetchEpics: expect.any(Function),
fetchSingleEpic: expect.any(Function),
},
]; ];
beforeEach(() => { beforeEach(() => {
...@@ -217,7 +231,7 @@ describe('RoadmapFilters', () => { ...@@ -217,7 +231,7 @@ describe('RoadmapFilters', () => {
expect(filteredSearchBar.props('recentSearchesStorageKey')).toBe('epics'); expect(filteredSearchBar.props('recentSearchesStorageKey')).toBe('epics');
}); });
it('includes `Author`, `Milestone`, `Confidential` and `Label` tokens when user is not logged in', () => { it('includes `Author`, `Milestone`, `Confidential`, `Epic` and `Label` tokens when user is not logged in', () => {
expect(filteredSearchBar.props('tokens')).toEqual(filterTokens); expect(filteredSearchBar.props('tokens')).toEqual(filterTokens);
}); });
......
...@@ -15283,6 +15283,9 @@ msgstr "" ...@@ -15283,6 +15283,9 @@ msgstr ""
msgid "GroupRoadmap|The roadmap shows the progress of your epics along a timeline" msgid "GroupRoadmap|The roadmap shows the progress of your epics along a timeline"
msgstr "" msgstr ""
msgid "GroupRoadmap|To make your epics appear in the roadmap, add start or due dates to them."
msgstr ""
msgid "GroupRoadmap|To view the roadmap, add a start or due date to one of the %{linkStart}child epics%{linkEnd}." msgid "GroupRoadmap|To view the roadmap, add a start or due date to one of the %{linkStart}child epics%{linkEnd}."
msgstr "" msgstr ""
...@@ -31551,6 +31554,9 @@ msgstr "" ...@@ -31551,6 +31554,9 @@ msgstr ""
msgid "There was a problem fetching emojis." msgid "There was a problem fetching emojis."
msgstr "" msgstr ""
msgid "There was a problem fetching epics."
msgstr ""
msgid "There was a problem fetching groups." msgid "There was a problem fetching groups."
msgstr "" msgstr ""
......
...@@ -4,6 +4,7 @@ import Api from '~/api'; ...@@ -4,6 +4,7 @@ import Api from '~/api';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue'; import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
...@@ -60,6 +61,11 @@ export const mockMilestones = [ ...@@ -60,6 +61,11 @@ export const mockMilestones = [
mockEscapedMilestone, mockEscapedMilestone,
]; ];
export const mockEpics = [
{ iid: 1, id: 1, title: 'Foo' },
{ iid: 2, id: 2, title: 'Bar' },
];
export const mockEmoji1 = { export const mockEmoji1 = {
name: 'thumbsup', name: 'thumbsup',
}; };
...@@ -114,6 +120,18 @@ export const mockMilestoneToken = { ...@@ -114,6 +120,18 @@ export const mockMilestoneToken = {
fetchMilestones: () => Promise.resolve({ data: mockMilestones }), fetchMilestones: () => Promise.resolve({ data: mockMilestones }),
}; };
export const mockEpicToken = {
type: 'epic_iid',
icon: 'clock',
title: 'Epic',
unique: true,
symbol: '&',
token: EpicToken,
operators: [{ value: '=', description: 'is', default: 'true' }],
fetchEpics: () => Promise.resolve({ data: mockEpics }),
fetchSingleEpic: () => Promise.resolve({ data: mockEpics[0] }),
};
export const mockReactionEmojiToken = { export const mockReactionEmojiToken = {
type: 'my_reaction_emoji', type: 'my_reaction_emoji',
icon: 'thumb-up', icon: 'thumb-up',
...@@ -189,6 +207,14 @@ export const tokenValuePlain = { ...@@ -189,6 +207,14 @@ export const tokenValuePlain = {
value: { data: 'foo' }, value: { data: 'foo' },
}; };
export const tokenValueEpic = {
type: 'epic_iid',
value: {
operator: '=',
data: '"foo"::&42',
},
};
export const mockHistoryItems = [ export const mockHistoryItems = [
[tokenValueAuthor, tokenValueLabel, tokenValueMilestone, 'duo'], [tokenValueAuthor, tokenValueLabel, tokenValueMilestone, 'duo'],
[tokenValueAuthor, 'si'], [tokenValueAuthor, 'si'],
......
import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue';
import { mockEpicToken, mockEpics } from '../mock_data';
jest.mock('~/flash');
const defaultStubs = {
Portal: true,
GlFilteredSearchSuggestionList: {
template: '<div></div>',
methods: {
getValue: () => '=',
},
},
};
function createComponent(options = {}) {
const {
config = mockEpicToken,
value = { data: '' },
active = false,
stubs = defaultStubs,
} = options;
return mount(EpicToken, {
propsData: {
config,
value,
active,
},
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: 'custom-class',
},
stubs,
});
}
describe('EpicToken', () => {
let mock;
let wrapper;
beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createComponent();
});
afterEach(() => {
mock.restore();
wrapper.destroy();
});
describe('computed', () => {
beforeEach(async () => {
wrapper = createComponent({
data: {
epics: mockEpics,
},
});
await wrapper.vm.$nextTick();
});
describe('currentValue', () => {
it.each`
data | id
${`${mockEpics[0].title}::&${mockEpics[0].iid}`} | ${mockEpics[0].iid}
${mockEpics[0].iid} | ${mockEpics[0].iid}
${'foobar'} | ${'foobar'}
`('$data returns $id', async ({ data, id }) => {
wrapper.setProps({ value: { data } });
await wrapper.vm.$nextTick();
expect(wrapper.vm.currentValue).toBe(id);
});
});
describe('activeEpic', () => {
it('returns object for currently present `value.data`', async () => {
wrapper.setProps({
value: { data: `${mockEpics[0].iid}` },
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.activeEpic).toEqual(mockEpics[0]);
});
});
});
describe('methods', () => {
describe('fetchEpicsBySearchTerm', () => {
it('calls `config.fetchEpics` with provided searchTerm param', () => {
jest.spyOn(wrapper.vm.config, 'fetchEpics');
wrapper.vm.fetchEpicsBySearchTerm('foo');
expect(wrapper.vm.config.fetchEpics).toHaveBeenCalledWith('foo');
});
it('sets response to `epics` when request is successful', async () => {
jest.spyOn(wrapper.vm.config, 'fetchEpics').mockResolvedValue({
data: mockEpics,
});
wrapper.vm.fetchEpicsBySearchTerm();
await waitForPromises();
expect(wrapper.vm.epics).toEqual(mockEpics);
});
it('calls `createFlash` with flash error message when request fails', async () => {
jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({});
wrapper.vm.fetchEpicsBySearchTerm('foo');
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
message: 'There was a problem fetching epics.',
});
});
it('sets `loading` to false when request completes', async () => {
jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({});
wrapper.vm.fetchEpicsBySearchTerm('foo');
await waitForPromises();
expect(wrapper.vm.loading).toBe(false);
});
});
describe('fetchSingleEpic', () => {
it('calls `config.fetchSingleEpic` with provided iid param', async () => {
jest.spyOn(wrapper.vm.config, 'fetchSingleEpic');
wrapper.vm.fetchSingleEpic(1);
expect(wrapper.vm.config.fetchSingleEpic).toHaveBeenCalledWith(1);
await waitForPromises();
expect(wrapper.vm.epics).toEqual([mockEpics[0]]);
});
});
});
describe('template', () => {
beforeEach(async () => {
wrapper = createComponent({
value: { data: `${mockEpics[0].iid}` },
data: { epics: mockEpics },
});
await wrapper.vm.$nextTick();
});
it('renders gl-filtered-search-token component', () => {
expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
});
it('renders token item when value is selected', () => {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3);
expect(tokenSegments.at(2).text()).toBe(`${mockEpics[0].title}::&${mockEpics[0].iid}`);
});
});
});
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