Commit fb245442 authored by Florie Guibert's avatar Florie Guibert Committed by Simon Knox

Refactor EpicsToken to use BaseToken

parent 3b3dce4a
...@@ -122,7 +122,7 @@ export default { ...@@ -122,7 +122,7 @@ export default {
fullPath: { fullPath: {
default: '', default: '',
}, },
groupEpicsPath: { groupPath: {
default: '', default: '',
}, },
hasAnyIssues: { hasAnyIssues: {
...@@ -371,16 +371,18 @@ export default { ...@@ -371,16 +371,18 @@ export default {
}); });
} }
if (this.groupEpicsPath) { if (this.groupPath) {
tokens.push({ tokens.push({
type: TOKEN_TYPE_EPIC, type: TOKEN_TYPE_EPIC,
title: TOKEN_TITLE_EPIC, title: TOKEN_TITLE_EPIC,
icon: 'epic', icon: 'epic',
token: EpicToken, token: EpicToken,
unique: true, unique: true,
symbol: '&',
idProperty: 'id', idProperty: 'id',
useIdValue: true, useIdValue: true,
fetchEpics: this.fetchEpics, recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-epic_id`,
fullPath: this.groupPath,
}); });
} }
...@@ -450,16 +452,6 @@ export default { ...@@ -450,16 +452,6 @@ export default {
fetchEmojis(search) { fetchEmojis(search) {
return this.fetchWithCache(this.autocompleteAwardEmojisPath, 'emojis', 'name', search); return this.fetchWithCache(this.autocompleteAwardEmojisPath, 'emojis', 'name', search);
}, },
async fetchEpics({ search }) {
const epics = await this.fetchWithCache(this.groupEpicsPath, 'epics');
if (!search) {
return epics.slice(0, MAX_LIST_SIZE);
}
const number = Number(search);
return Number.isNaN(number)
? fuzzaldrinPlus.filter(epics, search, { key: 'title' })
: epics.filter((epic) => epic.id === number);
},
fetchLabels(search) { fetchLabels(search) {
return this.$apollo return this.$apollo
.query({ .query({
......
...@@ -119,7 +119,7 @@ export function mountIssuesListApp() { ...@@ -119,7 +119,7 @@ export function mountIssuesListApp() {
emptyStateSvgPath, emptyStateSvgPath,
exportCsvPath, exportCsvPath,
fullPath, fullPath,
groupEpicsPath, groupPath,
hasAnyIssues, hasAnyIssues,
hasAnyProjects, hasAnyProjects,
hasBlockedIssuesFeature, hasBlockedIssuesFeature,
...@@ -152,7 +152,7 @@ export function mountIssuesListApp() { ...@@ -152,7 +152,7 @@ export function mountIssuesListApp() {
canBulkUpdate: parseBoolean(canBulkUpdate), canBulkUpdate: parseBoolean(canBulkUpdate),
emptyStateSvgPath, emptyStateSvgPath,
fullPath, fullPath,
groupEpicsPath, groupPath,
hasAnyIssues: parseBoolean(hasAnyIssues), hasAnyIssues: parseBoolean(hasAnyIssues),
hasAnyProjects: parseBoolean(hasAnyProjects), hasAnyProjects: parseBoolean(hasAnyProjects),
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature), hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
......
fragment EpicNode on Epic {
id
iid
group {
fullPath
}
title
state
reference
referencePath: reference(full: true)
webPath
webUrl
createdAt
closedAt
}
#import "./epic.fragment.graphql"
query searchEpics($fullPath: ID!, $search: String, $state: EpicState) {
group(fullPath: $fullPath) {
epics(
search: $search
state: $state
includeAncestorGroups: true
includeDescendantGroups: false
) {
nodes {
...EpicNode
}
}
}
}
...@@ -67,6 +67,11 @@ export default { ...@@ -67,6 +67,11 @@ export default {
required: false, required: false,
default: 'id', default: 'id',
}, },
searchBy: {
type: String,
required: false,
default: undefined,
},
}, },
data() { data() {
return { return {
...@@ -112,16 +117,18 @@ export default { ...@@ -112,16 +117,18 @@ export default {
); );
}, },
showDefaultSuggestions() { showDefaultSuggestions() {
return this.availableDefaultSuggestions.length; return this.availableDefaultSuggestions.length > 0;
}, },
showRecentSuggestions() { showRecentSuggestions() {
return this.isRecentSuggestionsEnabled && this.recentSuggestions.length && !this.searchKey; return (
this.isRecentSuggestionsEnabled && this.recentSuggestions.length > 0 && !this.searchKey
);
}, },
showPreloadedSuggestions() { showPreloadedSuggestions() {
return this.preloadedSuggestions.length && !this.searchKey; return this.preloadedSuggestions.length > 0 && !this.searchKey;
}, },
showAvailableSuggestions() { showAvailableSuggestions() {
return this.availableSuggestions.length; return this.availableSuggestions.length > 0;
}, },
showSuggestions() { showSuggestions() {
// These conditions must match the template under `#suggestions` slot // These conditions must match the template under `#suggestions` slot
...@@ -134,13 +141,19 @@ export default { ...@@ -134,13 +141,19 @@ export default {
this.showAvailableSuggestions this.showAvailableSuggestions
); );
}, },
searchTerm() {
return this.searchBy && this.activeTokenValue
? this.activeTokenValue[this.searchBy]
: undefined;
},
}, },
watch: { watch: {
active: { active: {
immediate: true, immediate: true,
handler(newValue) { handler(newValue) {
if (!newValue && !this.suggestions.length) { if (!newValue && !this.suggestions.length) {
this.$emit('fetch-suggestions', this.value.data); const search = this.searchTerm ? this.searchTerm : this.value.data;
this.$emit('fetch-suggestions', search);
} }
}, },
}, },
...@@ -150,7 +163,8 @@ export default { ...@@ -150,7 +163,8 @@ export default {
this.searchKey = data; this.searchKey = data;
if (!this.suggestionsLoading && !this.activeTokenValue) { if (!this.suggestionsLoading && !this.activeTokenValue) {
this.$emit('fetch-suggestions', data); const search = this.searchTerm ? this.searchTerm : data;
this.$emit('fetch-suggestions', search);
} }
}, DEBOUNCE_DELAY), }, DEBOUNCE_DELAY),
handleTokenValueSelected(activeTokenValue) { handleTokenValueSelected(activeTokenValue) {
......
<script> <script>
import { import { GlFilteredSearchSuggestion } from '@gitlab/ui';
GlDropdownDivider,
GlFilteredSearchSuggestion,
GlFilteredSearchToken,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants'; import { DEFAULT_NONE_ANY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
import searchEpicsQuery from '../queries/search_epics.query.graphql';
import BaseToken from './base_token.vue';
export default { export default {
separator: '::&', prefix: '&',
separator: '::',
components: { components: {
GlDropdownDivider, BaseToken,
GlFilteredSearchToken,
GlFilteredSearchSuggestion, GlFilteredSearchSuggestion,
GlLoadingIcon,
}, },
props: { props: {
config: { config: {
...@@ -27,11 +24,15 @@ export default { ...@@ -27,11 +24,15 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
active: {
type: Boolean,
required: true,
},
}, },
data() { data() {
return { return {
epics: this.config.initialEpics || [], epics: this.config.initialEpics || [],
loading: true, loading: false,
}; };
}, },
computed: { computed: {
...@@ -56,98 +57,73 @@ export default { ...@@ -56,98 +57,73 @@ export default {
} }
return this.defaultEpics; return this.defaultEpics;
}, },
activeEpic() {
if (this.currentValue && this.epics.length) {
// Check if current value is an epic ID.
if (typeof this.currentValue === 'number') {
return this.epics.find((epic) => epic[this.idProperty] === this.currentValue);
}
// Current value is a string.
const [groupPath, idProperty] = this.currentValue?.split(this.$options.separator);
return this.epics.find(
(epic) =>
epic.group_full_path === groupPath &&
epic[this.idProperty] === parseInt(idProperty, 10),
);
}
return null;
},
displayText() {
return `${this.activeEpic?.title}${this.$options.separator}${this.activeEpic?.iid}`;
},
},
watch: {
active: {
immediate: true,
handler(newValue) {
if (!newValue && !this.epics.length) {
this.searchEpics({ data: this.currentValue });
}
},
},
}, },
methods: { methods: {
fetchEpicsBySearchTerm({ epicPath = '', search = '' }) { fetchEpics(search = '') {
return this.$apollo
.query({
query: searchEpicsQuery,
variables: { fullPath: this.config.fullPath, search },
})
.then(({ data }) => data.group?.epics.nodes);
},
fetchEpicsBySearchTerm(search) {
this.loading = true; this.loading = true;
this.config this.fetchEpics(search)
.fetchEpics({ epicPath, search })
.then((response) => { .then((response) => {
this.epics = Array.isArray(response) ? response : response.data; this.epics = Array.isArray(response) ? response : response?.data;
}) })
.catch(() => createFlash({ message: __('There was a problem fetching epics.') })) .catch(() => createFlash({ message: __('There was a problem fetching epics.') }))
.finally(() => { .finally(() => {
this.loading = false; this.loading = false;
}); });
}, },
searchEpics: debounce(function debouncedSearch({ data }) { getActiveEpic(epics, data) {
let epicPath = this.activeEpic?.web_url; if (data && epics.length) {
return epics.find((epic) => this.getValue(epic) === data);
// When user visits the page with token value already included in filters
// We don't have any information about selected token except for its
// group path and iid joined by separator, so we need to manually
// compose epic path from it.
if (data.includes?.(this.$options.separator)) {
const [groupPath, epicIid] = data.split(this.$options.separator);
epicPath = `/groups/${groupPath}/-/epics/${epicIid}`;
} }
this.fetchEpicsBySearchTerm({ epicPath, search: data }); return undefined;
}, DEBOUNCE_DELAY), },
getValue(epic) { getValue(epic) {
return this.config.useIdValue return this.getEpicIdProperty(epic).toString();
? String(epic[this.idProperty]) },
: `${epic.group_full_path}${this.$options.separator}${epic[this.idProperty]}`; displayValue(epic) {
return `${this.$options.prefix}${this.getEpicIdProperty(epic)}${this.$options.separator}${
epic?.title
}`;
},
getEpicIdProperty(epic) {
return getIdFromGraphQLId(epic[this.idProperty]);
}, },
}, },
}; };
</script> </script>
<template> <template>
<gl-filtered-search-token <base-token
:config="config" :config="config"
v-bind="{ ...$props, ...$attrs }" :value="value"
:active="active"
:suggestions-loading="loading"
:suggestions="epics"
:get-active-token-value="getActiveEpic"
:default-suggestions="availableDefaultEpics"
:recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
search-by="title"
@fetch-suggestions="fetchEpicsBySearchTerm"
v-on="$listeners" v-on="$listeners"
@input="searchEpics"
> >
<template #view="{ inputValue }"> <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
{{ activeEpic ? displayText : inputValue }} {{ activeTokenValue ? displayValue(activeTokenValue) : inputValue }}
</template> </template>
<template #suggestions> <template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion <gl-filtered-search-suggestion
v-for="epic in availableDefaultEpics" v-for="epic in suggestions"
:key="epic.value" :key="epic.id"
:value="epic.value" :value="getValue(epic)"
> >
{{ epic.text }}
</gl-filtered-search-suggestion>
<gl-dropdown-divider v-if="availableDefaultEpics.length" />
<gl-loading-icon v-if="loading" size="sm" />
<template v-else>
<gl-filtered-search-suggestion v-for="epic in epics" :key="epic.id" :value="getValue(epic)">
{{ epic.title }} {{ epic.title }}
</gl-filtered-search-suggestion> </gl-filtered-search-suggestion>
</template> </template>
</template> </base-token>
</gl-filtered-search-token>
</template> </template>
...@@ -2,7 +2,6 @@ import { GlFilteredSearchToken } from '@gitlab/ui'; ...@@ -2,7 +2,6 @@ import { GlFilteredSearchToken } from '@gitlab/ui';
import Api from '~/api'; import Api from '~/api';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { import {
...@@ -150,29 +149,13 @@ export default { ...@@ -150,29 +149,13 @@ export default {
icon: 'epic', icon: 'epic',
title: __('Epic'), title: __('Epic'),
unique: true, unique: true,
idProperty: 'iid',
useIdValue: true,
symbol: '&', symbol: '&',
token: EpicToken, token: EpicToken,
operators: OPERATOR_IS_ONLY, operators: OPERATOR_IS_ONLY,
defaultEpics: [], recentSuggestionsStorageKey: `${this.groupFullPath}-epics-recent-tokens-epic_iid`,
fetchEpics: ({ epicPath = '', search = '' }) => { fullPath: this.groupFullPath,
const epicId = Number(search) || null;
// No search criteria or path has been provided, fetch all epics.
if (!epicPath && !search) {
return axios.get(this.listEpicsPath);
} else if (epicPath) {
// Just epicPath has been provided, fetch a specific epic.
return axios.get(epicPath).then(({ data }) => [data]);
} else if (!epicPath && epicId) {
// Exact epic ID provided, fetch the epic.
return axios
.get(joinPaths(this.listEpicsPath, String(epicId)))
.then(({ data }) => [data]);
}
// Search for an epic.
return axios.get(this.listEpicsPath, { params: { search } });
},
}); });
} }
......
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
import { visitUrl, mergeUrlParams, queryToObject } from '~/lib/utils/url_utility'; import { visitUrl, mergeUrlParams, queryToObject } from '~/lib/utils/url_utility';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
...@@ -28,6 +30,12 @@ export default () => { ...@@ -28,6 +30,12 @@ export default () => {
return false; return false;
} }
Vue.use(VueApollo);
const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
const apolloProvider = new VueApollo({
defaultClient,
});
// This event handler is to be removed in 11.1 once // This event handler is to be removed in 11.1 once
// we allow user to save selected preset in db // we allow user to save selected preset in db
if (presetButtonsContainer) { if (presetButtonsContainer) {
...@@ -43,7 +51,7 @@ export default () => { ...@@ -43,7 +51,7 @@ export default () => {
return new Vue({ return new Vue({
el, el,
apolloProvider: {}, apolloProvider,
store: createStore(), store: createStore(),
components: { components: {
roadmapApp, roadmapApp,
......
...@@ -57,7 +57,7 @@ module EE ...@@ -57,7 +57,7 @@ module EE
def project_issues_list_data(project, current_user, finder) def project_issues_list_data(project, current_user, finder)
super.tap do |data| super.tap do |data|
if project.feature_available?(:epics) && project.group if project.feature_available?(:epics) && project.group
data[:group_epics_path] = group_epics_path(project.group, format: :json) data[:group_path] = project.group.full_path
end end
end end
end end
...@@ -68,7 +68,7 @@ module EE ...@@ -68,7 +68,7 @@ module EE
data[:can_bulk_update] = (can?(current_user, :admin_issue, group) && group.feature_available?(:group_bulk_edit)).to_s data[:can_bulk_update] = (can?(current_user, :admin_issue, group) && group.feature_available?(:group_bulk_edit)).to_s
if group.feature_available?(:epics) if group.feature_available?(:epics)
data[:group_epics_path] = group_epics_path(group, format: :json) data[:group_path] = group.full_path
end end
end end
end end
......
...@@ -838,8 +838,10 @@ export const mockEpicTokenConfig = { ...@@ -838,8 +838,10 @@ export const mockEpicTokenConfig = {
symbol: '&', symbol: '&',
token: EpicToken, token: EpicToken,
operators: OPERATOR_IS_ONLY, operators: OPERATOR_IS_ONLY,
defaultEpics: [], idProperty: 'iid',
fetchEpics: expect.any(Function), useIdValue: true,
recentSuggestionsStorageKey: 'gitlab-org-epics-recent-tokens-epic_iid',
fullPath: 'gitlab-org',
}; };
export const mockReactionEmojiTokenConfig = { export const mockReactionEmojiTokenConfig = {
......
...@@ -147,7 +147,7 @@ RSpec.describe EE::IssuesHelper do ...@@ -147,7 +147,7 @@ RSpec.describe EE::IssuesHelper do
has_issue_weights_feature: 'true', has_issue_weights_feature: 'true',
has_iterations_feature: 'true', has_iterations_feature: 'true',
has_multiple_issue_assignees_feature: 'true', has_multiple_issue_assignees_feature: 'true',
group_epics_path: group_epics_path(project.group, format: :json) group_path: project.group.full_path
} }
expect(helper.project_issues_list_data(project, current_user, finder)).to include(expected) expect(helper.project_issues_list_data(project, current_user, finder)).to include(expected)
...@@ -156,8 +156,8 @@ RSpec.describe EE::IssuesHelper do ...@@ -156,8 +156,8 @@ RSpec.describe EE::IssuesHelper do
context 'when project does not have group' do context 'when project does not have group' do
let(:project_with_no_group) { create :project } let(:project_with_no_group) { create :project }
it 'does not return group_epics_path' do it 'does not return group_path' do
expect(helper.project_issues_list_data(project_with_no_group, current_user, finder)).not_to include(:group_epics_path) expect(helper.project_issues_list_data(project_with_no_group, current_user, finder)).not_to include(:group_path)
end end
end end
end end
...@@ -179,7 +179,7 @@ RSpec.describe EE::IssuesHelper do ...@@ -179,7 +179,7 @@ RSpec.describe EE::IssuesHelper do
result = helper.project_issues_list_data(project, current_user, finder) result = helper.project_issues_list_data(project, current_user, finder)
expect(result).to include(expected) expect(result).to include(expected)
expect(result).not_to include(:group_epics_path) expect(result).not_to include(:group_path)
end end
end end
end end
...@@ -208,7 +208,7 @@ RSpec.describe EE::IssuesHelper do ...@@ -208,7 +208,7 @@ RSpec.describe EE::IssuesHelper do
has_issue_weights_feature: 'true', has_issue_weights_feature: 'true',
has_iterations_feature: 'true', has_iterations_feature: 'true',
has_multiple_issue_assignees_feature: 'true', has_multiple_issue_assignees_feature: 'true',
group_epics_path: group_epics_path(project.group, format: :json) group_path: project.group.full_path
} }
expect(helper.group_issues_list_data(group, current_user, issues, projects)).to include(expected) expect(helper.group_issues_list_data(group, current_user, issues, projects)).to include(expected)
...@@ -233,7 +233,7 @@ RSpec.describe EE::IssuesHelper do ...@@ -233,7 +233,7 @@ RSpec.describe EE::IssuesHelper do
result = helper.group_issues_list_data(group, current_user, issues, projects) result = helper.group_issues_list_data(group, current_user, issues, projects)
expect(result).to include(expected) expect(result).to include(expected)
expect(result).not_to include(:group_epics_path) expect(result).not_to include(:group_path)
end end
end end
end end
......
...@@ -520,7 +520,7 @@ describe('IssuesListApp component', () => { ...@@ -520,7 +520,7 @@ describe('IssuesListApp component', () => {
beforeEach(() => { beforeEach(() => {
wrapper = mountComponent({ wrapper = mountComponent({
provide: { provide: {
groupEpicsPath: '', groupPath: '',
}, },
}); });
}); });
...@@ -536,7 +536,7 @@ describe('IssuesListApp component', () => { ...@@ -536,7 +536,7 @@ describe('IssuesListApp component', () => {
beforeEach(() => { beforeEach(() => {
wrapper = mountComponent({ wrapper = mountComponent({
provide: { provide: {
groupEpicsPath: '', groupPath: '',
}, },
}); });
}); });
...@@ -564,7 +564,7 @@ describe('IssuesListApp component', () => { ...@@ -564,7 +564,7 @@ describe('IssuesListApp component', () => {
provide: { provide: {
isSignedIn: true, isSignedIn: true,
projectIterationsPath: 'project/iterations/path', projectIterationsPath: 'project/iterations/path',
groupEpicsPath: 'group/epics/path', groupPath: 'group/path',
hasIssueWeightsFeature: true, hasIssueWeightsFeature: true,
}, },
}); });
......
...@@ -141,7 +141,62 @@ export const mockEpicToken = { ...@@ -141,7 +141,62 @@ export const mockEpicToken = {
token: EpicToken, token: EpicToken,
operators: OPERATOR_IS_ONLY, operators: OPERATOR_IS_ONLY,
idProperty: 'iid', idProperty: 'iid',
fetchEpics: () => Promise.resolve({ data: mockEpics }), fullPath: 'gitlab-org',
};
export const mockEpicNode1 = {
__typename: 'Epic',
parent: null,
id: 'gid://gitlab/Epic/40',
iid: '2',
title: 'Marketing epic',
description: 'Mock epic description',
state: 'opened',
startDate: '2017-12-25',
dueDate: '2018-02-15',
webUrl: 'http://gdk.test:3000/groups/gitlab-org/marketing/-/epics/1',
hasChildren: false,
hasParent: false,
confidential: false,
};
export const mockEpicNode2 = {
__typename: 'Epic',
parent: null,
id: 'gid://gitlab/Epic/41',
iid: '3',
title: 'Another marketing',
startDate: '2017-12-26',
dueDate: '2018-03-10',
state: 'opened',
webUrl: 'http://gdk.test:3000/groups/gitlab-org/marketing/-/epics/2',
};
export const mockGroupEpicsQueryResponse = {
data: {
group: {
id: 'gid://gitlab/Group/1',
name: 'Gitlab Org',
epics: {
edges: [
{
node: {
...mockEpicNode1,
},
__typename: 'EpicEdge',
},
{
node: {
...mockEpicNode2,
},
__typename: 'EpicEdge',
},
],
__typename: 'EpicConnection',
},
__typename: 'Group',
},
},
}; };
export const mockReactionEmojiToken = { export const mockReactionEmojiToken = {
......
import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'; import { GlFilteredSearchTokenSegment } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash'; import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import searchEpicsQuery from '~/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql';
import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue'; import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { mockEpicToken, mockEpics } from '../mock_data'; import { mockEpicToken, mockEpics, mockGroupEpicsQueryResponse } from '../mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
Vue.use(VueApollo);
const defaultStubs = { const defaultStubs = {
Portal: true, Portal: true,
...@@ -21,7 +27,18 @@ const defaultStubs = { ...@@ -21,7 +27,18 @@ const defaultStubs = {
}, },
}; };
function createComponent(options = {}) { describe('EpicToken', () => {
let mock;
let wrapper;
let fakeApollo;
const findBaseToken = () => wrapper.findComponent(BaseToken);
function createComponent(
options = {},
epicsQueryHandler = jest.fn().mockResolvedValue(mockGroupEpicsQueryResponse),
) {
fakeApollo = createMockApollo([[searchEpicsQuery, epicsQueryHandler]]);
const { const {
config = mockEpicToken, config = mockEpicToken,
value = { data: '' }, value = { data: '' },
...@@ -29,6 +46,7 @@ function createComponent(options = {}) { ...@@ -29,6 +46,7 @@ function createComponent(options = {}) {
stubs = defaultStubs, stubs = defaultStubs,
} = options; } = options;
return mount(EpicToken, { return mount(EpicToken, {
apolloProvider: fakeApollo,
propsData: { propsData: {
config, config,
value, value,
...@@ -37,15 +55,11 @@ function createComponent(options = {}) { ...@@ -37,15 +55,11 @@ function createComponent(options = {}) {
provide: { provide: {
portalName: 'fake target', portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {}, alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class', suggestionsListClass: 'custom-class',
}, },
stubs, stubs,
}); });
} }
describe('EpicToken', () => {
let mock;
let wrapper;
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
...@@ -71,23 +85,20 @@ describe('EpicToken', () => { ...@@ -71,23 +85,20 @@ describe('EpicToken', () => {
describe('methods', () => { describe('methods', () => {
describe('fetchEpicsBySearchTerm', () => { describe('fetchEpicsBySearchTerm', () => {
it('calls `config.fetchEpics` with provided searchTerm param', () => { it('calls fetchEpics with provided searchTerm param', () => {
jest.spyOn(wrapper.vm.config, 'fetchEpics'); jest.spyOn(wrapper.vm, 'fetchEpics');
wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' }); findBaseToken().vm.$emit('fetch-suggestions', 'foo');
expect(wrapper.vm.config.fetchEpics).toHaveBeenCalledWith({ expect(wrapper.vm.fetchEpics).toHaveBeenCalledWith('foo');
epicPath: '',
search: 'foo',
});
}); });
it('sets response to `epics` when request is successful', async () => { it('sets response to `epics` when request is successful', async () => {
jest.spyOn(wrapper.vm.config, 'fetchEpics').mockResolvedValue({ jest.spyOn(wrapper.vm, 'fetchEpics').mockResolvedValue({
data: mockEpics, data: mockEpics,
}); });
wrapper.vm.fetchEpicsBySearchTerm({}); findBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises(); await waitForPromises();
...@@ -95,9 +106,9 @@ describe('EpicToken', () => { ...@@ -95,9 +106,9 @@ describe('EpicToken', () => {
}); });
it('calls `createFlash` with flash error message when request fails', async () => { it('calls `createFlash` with flash error message when request fails', async () => {
jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({}); jest.spyOn(wrapper.vm, 'fetchEpics').mockRejectedValue({});
wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' }); findBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises(); await waitForPromises();
...@@ -107,9 +118,9 @@ describe('EpicToken', () => { ...@@ -107,9 +118,9 @@ describe('EpicToken', () => {
}); });
it('sets `loading` to false when request completes', async () => { it('sets `loading` to false when request completes', async () => {
jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({}); jest.spyOn(wrapper.vm, 'fetchEpics').mockRejectedValue({});
wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' }); findBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises(); await waitForPromises();
...@@ -123,15 +134,15 @@ describe('EpicToken', () => { ...@@ -123,15 +134,15 @@ describe('EpicToken', () => {
beforeEach(async () => { beforeEach(async () => {
wrapper = createComponent({ wrapper = createComponent({
value: { data: `${mockEpics[0].group_full_path}::&${mockEpics[0].iid}` }, value: { data: `${mockEpics[0].title}::&${mockEpics[0].iid}` },
data: { epics: mockEpics }, data: { epics: mockEpics },
}); });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
}); });
it('renders gl-filtered-search-token component', () => { it('renders BaseToken component', () => {
expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true); expect(findBaseToken().exists()).toBe(true);
}); });
it('renders token item when value is selected', () => { it('renders token item when value is selected', () => {
...@@ -143,8 +154,8 @@ describe('EpicToken', () => { ...@@ -143,8 +154,8 @@ describe('EpicToken', () => {
it.each` it.each`
value | valueType | tokenValueString value | valueType | tokenValueString
${`${mockEpics[0].group_full_path}::&${mockEpics[0].iid}`} | ${'string'} | ${`${mockEpics[0].title}::&${mockEpics[0].iid}`} ${`${mockEpics[0].title}::&${mockEpics[0].iid}`} | ${'string'} | ${`${mockEpics[0].title}::&${mockEpics[0].iid}`}
${`${mockEpics[1].group_full_path}::&${mockEpics[1].iid}`} | ${'number'} | ${`${mockEpics[1].title}::&${mockEpics[1].iid}`} ${`${mockEpics[1].title}::&${mockEpics[1].iid}`} | ${'number'} | ${`${mockEpics[1].title}::&${mockEpics[1].iid}`}
`('renders token item when selection is a $valueType', async ({ value, tokenValueString }) => { `('renders token item when selection is a $valueType', async ({ value, tokenValueString }) => {
wrapper.setProps({ wrapper.setProps({
value: { data: value }, value: { data: value },
......
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