Commit 6b639561 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '229266-mlunoe-add-branch-filter-to-merge-request-analytics' into 'master'

Feat(Merge Request Analytics): add branch filter

Closes #229266

See merge request gitlab-org/gitlab!41898
parents f0081b34 182b76f2
<script>
import {
GlToken,
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { DEBOUNCE_DELAY } from '../constants';
export default {
components: {
GlToken,
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlLoadingIcon,
},
props: {
config: {
type: Object,
required: true,
},
value: {
type: Object,
required: true,
},
},
data() {
return {
branches: this.config.initialBranches || [],
defaultBranches: this.config.defaultBranches || [],
loading: true,
};
},
computed: {
currentValue() {
return this.value.data.toLowerCase();
},
activeBranch() {
return this.branches.find(branch => branch.name.toLowerCase() === this.currentValue);
},
},
watch: {
active: {
immediate: true,
handler(newValue) {
if (!newValue && !this.branches.length) {
this.fetchBranchBySearchTerm(this.value.data);
}
},
},
},
methods: {
fetchBranchBySearchTerm(searchTerm) {
this.loading = true;
this.config
.fetchBranches(searchTerm)
.then(({ data }) => {
this.branches = data;
})
.catch(() => createFlash({ message: __('There was a problem fetching branches.') }))
.finally(() => {
this.loading = false;
});
},
searchBranches: debounce(function debouncedSearch({ data }) {
this.fetchBranchBySearchTerm(data);
}, DEBOUNCE_DELAY),
},
};
</script>
<template>
<gl-filtered-search-token
:config="config"
v-bind="{ ...$props, ...$attrs }"
v-on="$listeners"
@input="searchBranches"
>
<template #view-token="{ inputValue }">
<gl-token variant="search-value">{{
activeBranch ? activeBranch.name : inputValue
}}</gl-token>
</template>
<template #suggestions>
<gl-filtered-search-suggestion
v-for="branch in defaultBranches"
:key="branch.value"
:value="branch.value"
>
{{ branch.text }}
</gl-filtered-search-suggestion>
<gl-dropdown-divider v-if="defaultBranches.length" />
<gl-loading-icon v-if="loading" />
<template v-else>
<gl-filtered-search-suggestion
v-for="branch in branches"
:key="branch.id"
:value="branch.name"
>
<div class="gl-display-flex">
<span class="gl-display-inline-block gl-mr-3 gl-p-3"></span>
<div>{{ branch.name }}</div>
</div>
</gl-filtered-search-suggestion>
</template>
</template>
</gl-filtered-search-token>
</template>
...@@ -3,6 +3,7 @@ import { mapActions, mapState } from 'vuex'; ...@@ -3,6 +3,7 @@ import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale'; import { __ } from '~/locale';
import UrlSync from '~/vue_shared/components/url_sync.vue'; import UrlSync from '~/vue_shared/components/url_sync.vue';
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 BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_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 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 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';
...@@ -25,17 +26,40 @@ export default { ...@@ -25,17 +26,40 @@ export default {
inject: ['fullPath', 'type'], inject: ['fullPath', 'type'],
computed: { computed: {
...mapState('filters', { ...mapState('filters', {
selectedSourceBranch: state => state.branches.source.selected,
selectedTargetBranch: state => state.branches.target.selected,
selectedMilestone: state => state.milestones.selected, selectedMilestone: state => state.milestones.selected,
selectedAuthor: state => state.authors.selected, selectedAuthor: state => state.authors.selected,
selectedLabelList: state => state.labels.selectedList,
selectedAssignee: state => state.assignees.selected, selectedAssignee: state => state.assignees.selected,
selectedLabelList: state => state.labels.selectedList,
milestonesData: state => state.milestones.data, milestonesData: state => state.milestones.data,
labelsData: state => state.labels.data, labelsData: state => state.labels.data,
authorsData: state => state.authors.data,
assigneesData: state => state.assignees.data, assigneesData: state => state.assignees.data,
authorsData: state => state.authors.data,
branchesData: state => state.branches.data,
}), }),
tokens() { tokens() {
return [ return [
{
icon: 'branch',
title: __('Source Branch'),
type: 'source_branch',
token: BranchToken,
initialBranches: this.branchesData,
unique: true,
operators: [{ value: '=', description: 'is', default: 'true' }],
fetchBranches: this.fetchBranches,
},
{
icon: 'branch',
title: __('Target Branch'),
type: 'target_branch',
token: BranchToken,
initialBranches: this.branchesData,
unique: true,
operators: [{ value: '=', description: 'is', default: 'true' }],
fetchBranches: this.fetchBranches,
},
{ {
icon: 'clock', icon: 'clock',
title: __('Milestone'), title: __('Milestone'),
...@@ -85,6 +109,8 @@ export default { ...@@ -85,6 +109,8 @@ export default {
}, },
query() { query() {
return filterToQueryObject({ return filterToQueryObject({
source_branch_name: this.selectedSourceBranch,
target_branch_name: this.selectedTargetBranch,
milestone_title: this.selectedMilestone, milestone_title: this.selectedMilestone,
label_name: this.selectedLabelList, label_name: this.selectedLabelList,
author_username: this.selectedAuthor, author_username: this.selectedAuthor,
...@@ -93,6 +119,8 @@ export default { ...@@ -93,6 +119,8 @@ export default {
}, },
initialFilterValue() { initialFilterValue() {
return prepareTokens({ return prepareTokens({
source_branch: this.selectedSourceBranch,
target_branch: this.selectedTargetBranch,
milestone: this.selectedMilestone, milestone: this.selectedMilestone,
author: this.selectedAuthor, author: this.selectedAuthor,
assignee: this.selectedAssignee, assignee: this.selectedAssignee,
...@@ -103,15 +131,25 @@ export default { ...@@ -103,15 +131,25 @@ export default {
methods: { methods: {
...mapActions('filters', [ ...mapActions('filters', [
'setFilters', 'setFilters',
'fetchBranches',
'fetchMilestones', 'fetchMilestones',
'fetchLabels',
'fetchAuthors', 'fetchAuthors',
'fetchAssignees', 'fetchAssignees',
'fetchLabels',
]), ]),
handleFilter(filters) { handleFilter(filters) {
const { labels, milestone, author, assignee } = processFilters(filters); const {
source_branch: sourceBranch,
target_branch: targetBranch,
milestone,
author,
assignee,
labels,
} = processFilters(filters);
this.setFilters({ this.setFilters({
selectedSourceBranch: sourceBranch ? sourceBranch[0] : null,
selectedTargetBranch: targetBranch ? targetBranch[0] : null,
selectedAuthor: author ? author[0] : null, selectedAuthor: author ? author[0] : null,
selectedMilestone: milestone ? milestone[0] : null, selectedMilestone: milestone ? milestone[0] : null,
selectedAssignee: assignee ? assignee[0] : null, selectedAssignee: assignee ? assignee[0] : null,
......
...@@ -38,10 +38,12 @@ export default { ...@@ -38,10 +38,12 @@ export default {
}, },
variables() { variables() {
const options = filterToQueryObject({ const options = filterToQueryObject({
labels: this.selectedLabelList, sourceBranches: this.selectedSourceBranch,
targetBranches: this.selectedTargetBranch,
milestoneTitle: this.selectedMilestone,
authorUsername: this.selectedAuthor, authorUsername: this.selectedAuthor,
assigneeUsername: this.selectedAssignee, assigneeUsername: this.selectedAssignee,
milestoneTitle: this.selectedMilestone, labels: this.selectedLabelList,
}); });
return { return {
...@@ -59,10 +61,12 @@ export default { ...@@ -59,10 +61,12 @@ export default {
}, },
computed: { computed: {
...mapState('filters', { ...mapState('filters', {
selectedSourceBranch: state => state.branches.source.selected,
selectedTargetBranch: state => state.branches.target.selected,
selectedMilestone: state => state.milestones.selected, selectedMilestone: state => state.milestones.selected,
selectedAuthor: state => state.authors.selected, selectedAuthor: state => state.authors.selected,
selectedLabelList: state => state.labels.selectedList,
selectedAssignee: state => state.assignees.selected, selectedAssignee: state => state.assignees.selected,
selectedLabelList: state => state.labels.selectedList,
}), }),
chartOptions() { chartOptions() {
return { return {
......
...@@ -117,10 +117,12 @@ export default { ...@@ -117,10 +117,12 @@ export default {
query: throughputTableQuery, query: throughputTableQuery,
variables() { variables() {
const options = filterToQueryObject({ const options = filterToQueryObject({
labels: this.selectedLabelList, sourceBranches: this.selectedSourceBranch,
targetBranches: this.selectedTargetBranch,
milestoneTitle: this.selectedMilestone,
authorUsername: this.selectedAuthor, authorUsername: this.selectedAuthor,
assigneeUsername: this.selectedAssignee, assigneeUsername: this.selectedAssignee,
milestoneTitle: this.selectedMilestone, labels: this.selectedLabelList,
}); });
return { return {
...@@ -142,10 +144,12 @@ export default { ...@@ -142,10 +144,12 @@ export default {
}, },
computed: { computed: {
...mapState('filters', { ...mapState('filters', {
selectedSourceBranch: state => state.branches.source.selected,
selectedTargetBranch: state => state.branches.target.selected,
selectedMilestone: state => state.milestones.selected, selectedMilestone: state => state.milestones.selected,
selectedAuthor: state => state.authors.selected, selectedAuthor: state => state.authors.selected,
selectedLabelList: state => state.labels.selectedList,
selectedAssignee: state => state.assignees.selected, selectedAssignee: state => state.assignees.selected,
selectedLabelList: state => state.labels.selectedList,
}), }),
tableDataAvailable() { tableDataAvailable() {
return this.throughputTableData.length; return this.throughputTableData.length;
......
...@@ -7,6 +7,8 @@ query( ...@@ -7,6 +7,8 @@ query(
$authorUsername: String $authorUsername: String
$assigneeUsername: String $assigneeUsername: String
$milestoneTitle: String $milestoneTitle: String
$sourceBranches: [String!]
$targetBranches: [String!]
) { ) {
project(fullPath: $fullPath) { project(fullPath: $fullPath) {
mergeRequests( mergeRequests(
...@@ -18,6 +20,8 @@ query( ...@@ -18,6 +20,8 @@ query(
authorUsername: $authorUsername authorUsername: $authorUsername
assigneeUsername: $assigneeUsername assigneeUsername: $assigneeUsername
milestoneTitle: $milestoneTitle milestoneTitle: $milestoneTitle
sourceBranches: $sourceBranches
targetBranches: $targetBranches
) { ) {
nodes { nodes {
iid iid
......
...@@ -21,11 +21,31 @@ export default (startDate = null, endDate = null) => { ...@@ -21,11 +21,31 @@ export default (startDate = null, endDate = null) => {
// first: 0 is an optimization which makes sure we don't load merge request objects into memory (backend). // first: 0 is an optimization which makes sure we don't load merge request objects into memory (backend).
// Currently when requesting counts we also load the first 100 records (preloader problem). // Currently when requesting counts we also load the first 100 records (preloader problem).
return `${month}_${year}: mergeRequests(first: 0, mergedBefore: "${mergedBefore}", mergedAfter: "${mergedAfter}", labels: $labels, authorUsername: $authorUsername, assigneeUsername: $assigneeUsername, milestoneTitle: $milestoneTitle) { count }`; return `
${month}_${year}: mergeRequests(
first: 0,
mergedBefore: "${mergedBefore}",
mergedAfter: "${mergedAfter}",
labels: $labels,
authorUsername: $authorUsername,
assigneeUsername: $assigneeUsername,
milestoneTitle: $milestoneTitle,
sourceBranches: $sourceBranches,
targetBranches: $targetBranches
) { count }
`;
}); });
return gql` return gql`
query($fullPath: ID!, $labels: [String!], $authorUsername: String, $assigneeUsername: String, $milestoneTitle: String) { query(
$fullPath: ID!,
$labels: [String!],
$authorUsername: String,
$assigneeUsername: String,
$milestoneTitle: String,
$sourceBranches: [String!],
$targetBranches: [String!]
) {
throughputChartData: project(fullPath: $fullPath) { throughputChartData: project(fullPath: $fullPath) {
${computedMonthData} ${computedMonthData}
} }
......
...@@ -27,12 +27,16 @@ export default () => { ...@@ -27,12 +27,16 @@ export default () => {
projectEndpoint: type === ITEM_TYPE.PROJECT ? fullPath : null, projectEndpoint: type === ITEM_TYPE.PROJECT ? fullPath : null,
}); });
const { const {
source_branch_name = null,
target_branch_name = null,
assignee_username = null, assignee_username = null,
author_username = null, author_username = null,
milestone_title = null, milestone_title = null,
label_name = [], label_name = [],
} = urlQueryToFilter(window.location.search); } = urlQueryToFilter(window.location.search);
store.dispatch('filters/initialize', { store.dispatch('filters/initialize', {
selectedSourceBranch: source_branch_name,
selectedTargetBranch: target_branch_name,
selectedAssignee: assignee_username, selectedAssignee: assignee_username,
selectedAuthor: author_username, selectedAuthor: author_username,
selectedMilestone: milestone_title, selectedMilestone: milestone_title,
......
...@@ -12,6 +12,22 @@ export const setEndpoints = ({ commit }, params) => { ...@@ -12,6 +12,22 @@ export const setEndpoints = ({ commit }, params) => {
commit(types.SET_PROJECT_ENDPOINT, projectEndpoint); commit(types.SET_PROJECT_ENDPOINT, projectEndpoint);
}; };
export function fetchBranches({ commit, state }, search = '') {
const { projectEndpoint } = state;
commit(types.REQUEST_BRANCHES);
return Api.branches(projectEndpoint, search)
.then(response => {
commit(types.RECEIVE_BRANCHES_SUCCESS, response.data);
return response;
})
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_BRANCHES_ERROR, status);
createFlash(__('Failed to load branches. Please try again.'));
});
}
export const fetchMilestones = ({ commit, state }, search_title = '') => { export const fetchMilestones = ({ commit, state }, search_title = '') => {
commit(types.REQUEST_MILESTONES); commit(types.REQUEST_MILESTONES);
const { milestonesEndpoint } = state; const { milestonesEndpoint } = state;
......
...@@ -3,6 +3,10 @@ export const SET_LABELS_ENDPOINT = 'SET_LABELS_ENDPOINT'; ...@@ -3,6 +3,10 @@ export const SET_LABELS_ENDPOINT = 'SET_LABELS_ENDPOINT';
export const SET_GROUP_ENDPOINT = 'SET_GROUP_ENDPOINT'; export const SET_GROUP_ENDPOINT = 'SET_GROUP_ENDPOINT';
export const SET_PROJECT_ENDPOINT = 'SET_PROJECT_ENDPOINT'; export const SET_PROJECT_ENDPOINT = 'SET_PROJECT_ENDPOINT';
export const REQUEST_BRANCHES = 'REQUEST_BRANCHES';
export const RECEIVE_BRANCHES_SUCCESS = 'RECEIVE_BRANCHES_SUCCESS';
export const RECEIVE_BRANCHES_ERROR = 'RECEIVE_BRANCHES_ERROR';
export const REQUEST_MILESTONES = 'REQUEST_MILESTONES'; export const REQUEST_MILESTONES = 'REQUEST_MILESTONES';
export const RECEIVE_MILESTONES_SUCCESS = 'RECEIVE_MILESTONES_SUCCESS'; export const RECEIVE_MILESTONES_SUCCESS = 'RECEIVE_MILESTONES_SUCCESS';
export const RECEIVE_MILESTONES_ERROR = 'RECEIVE_MILESTONES_ERROR'; export const RECEIVE_MILESTONES_ERROR = 'RECEIVE_MILESTONES_ERROR';
......
...@@ -3,6 +3,10 @@ import * as types from './mutation_types'; ...@@ -3,6 +3,10 @@ import * as types from './mutation_types';
export default { export default {
[types.SET_SELECTED_FILTERS](state, params) { [types.SET_SELECTED_FILTERS](state, params) {
const { const {
selectedSourceBranch = null,
selectedSourceBranchList = [],
selectedTargetBranch = null,
selectedTargetBranchList = [],
selectedAuthor = null, selectedAuthor = null,
selectedAuthorList = [], selectedAuthorList = [],
selectedMilestone = null, selectedMilestone = null,
...@@ -12,6 +16,10 @@ export default { ...@@ -12,6 +16,10 @@ export default {
selectedLabel = null, selectedLabel = null,
selectedLabelList = [], selectedLabelList = [],
} = params; } = params;
state.branches.source.selected = selectedSourceBranch;
state.branches.source.selectedList = selectedSourceBranchList;
state.branches.target.selected = selectedTargetBranch;
state.branches.target.selectedList = selectedTargetBranchList;
state.authors.selected = selectedAuthor; state.authors.selected = selectedAuthor;
state.authors.selectedList = selectedAuthorList; state.authors.selectedList = selectedAuthorList;
state.assignees.selected = selectedAssignee; state.assignees.selected = selectedAssignee;
...@@ -85,4 +93,17 @@ export default { ...@@ -85,4 +93,17 @@ export default {
state.assignees.errorCode = errorCode; state.assignees.errorCode = errorCode;
state.assignees.data = []; state.assignees.data = [];
}, },
[types.REQUEST_BRANCHES](state) {
state.branches.isLoading = true;
},
[types.RECEIVE_BRANCHES_SUCCESS](state, data) {
state.branches.isLoading = false;
state.branches.data = data;
state.branches.errorCode = null;
},
[types.RECEIVE_BRANCHES_ERROR](state, errorCode) {
state.branches.isLoading = false;
state.branches.errorCode = errorCode;
state.branches.data = [];
},
}; };
...@@ -3,6 +3,19 @@ export default () => ({ ...@@ -3,6 +3,19 @@ export default () => ({
labelsEndpoint: '', labelsEndpoint: '',
groupEndpoint: '', groupEndpoint: '',
projectEndpoint: '', projectEndpoint: '',
branches: {
isLoading: false,
errorCode: null,
data: [],
source: {
selected: null,
selectedList: [],
},
target: {
selected: null,
selectedList: [],
},
},
milestones: { milestones: {
isLoading: false, isLoading: false,
errorCode: null, errorCode: null,
......
...@@ -5,27 +5,32 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -5,27 +5,32 @@ import MockAdapter from 'axios-mock-adapter';
import storeConfig from 'ee/analytics/merge_request_analytics/store'; import storeConfig from 'ee/analytics/merge_request_analytics/store';
import FilterBar from 'ee/analytics/merge_request_analytics/components/filter_bar.vue'; import FilterBar from 'ee/analytics/merge_request_analytics/components/filter_bar.vue';
import initialFiltersState from 'ee/analytics/shared/store/modules/filters/state'; import initialFiltersState from 'ee/analytics/shared/store/modules/filters/state';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
import * as utils from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; import * as utils from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
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 UrlSync from '~/vue_shared/components/url_sync.vue'; import UrlSync from '~/vue_shared/components/url_sync.vue';
import * as commonUtils from '~/lib/utils/common_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import { ITEM_TYPE } from '~/groups/constants';
import { import {
filterMilestones, filterMilestones,
filterLabels, filterLabels,
filterUsers, filterUsers,
} from '../../shared/store/modules/filters/mock_data'; } from '../../shared/store/modules/filters/mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import { ITEM_TYPE } from '~/groups/constants';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
const sourceBranchTokenType = 'source_branch';
const targetBranchTokenType = 'target_branch';
const milestoneTokenType = 'milestone'; const milestoneTokenType = 'milestone';
const labelsTokenType = 'labels'; const labelsTokenType = 'labels';
const authorTokenType = 'author'; const authorTokenType = 'author';
const assigneeTokenType = 'assignee'; const assigneeTokenType = 'assignee';
const initialFilterBarState = { const initialFilterBarState = {
selectedSourceBranch: null,
selectedTargetBranch: null,
selectedMilestone: null, selectedMilestone: null,
selectedAuthor: null, selectedAuthor: null,
selectedAssignee: null, selectedAssignee: null,
...@@ -33,6 +38,10 @@ const initialFilterBarState = { ...@@ -33,6 +38,10 @@ const initialFilterBarState = {
}; };
const defaultParams = { const defaultParams = {
source_branch_name: null,
'not[source_branch_name]': null,
target_branch_name: null,
'not[target_branch_name]': null,
milestone_title: null, milestone_title: null,
'not[milestone_title]': null, 'not[milestone_title]': null,
author_username: null, author_username: null,
...@@ -63,10 +72,12 @@ function getFilterValues(tokens, options = {}) { ...@@ -63,10 +72,12 @@ function getFilterValues(tokens, options = {}) {
return tokens.map(token => token[prop]); return tokens.map(token => token[prop]);
} }
const selectedBranchParams = getFilterParams(mockBranches, { prop: 'name' });
const selectedMilestoneParams = getFilterParams(filterMilestones); const selectedMilestoneParams = getFilterParams(filterMilestones);
const selectedLabelParams = getFilterParams(filterLabels); const selectedLabelParams = getFilterParams(filterLabels);
const selectedUserParams = getFilterParams(filterUsers, { prop: 'name' }); const selectedUserParams = getFilterParams(filterUsers, { prop: 'name' });
const branchValues = getFilterValues(mockBranches, { prop: 'name' });
const milestoneValues = getFilterValues(filterMilestones); const milestoneValues = getFilterValues(filterMilestones);
const labelValues = getFilterValues(filterLabels); const labelValues = getFilterValues(filterLabels);
const userValues = getFilterValues(filterUsers, { prop: 'name' }); const userValues = getFilterValues(filterUsers, { prop: 'name' });
...@@ -141,6 +152,7 @@ describe('Filter bar', () => { ...@@ -141,6 +152,7 @@ describe('Filter bar', () => {
describe('when the state has data', () => { describe('when the state has data', () => {
beforeEach(() => { beforeEach(() => {
vuexStore = createStore({ vuexStore = createStore({
branches: { data: mockBranches, target: {}, source: {} },
milestones: { data: filterMilestones }, milestones: { data: filterMilestones },
labels: { data: filterLabels }, labels: { data: filterLabels },
authors: { data: userValues }, authors: { data: userValues },
...@@ -151,35 +163,53 @@ describe('Filter bar', () => { ...@@ -151,35 +163,53 @@ describe('Filter bar', () => {
it('displays the milestone, label, author and assignee tokens', () => { it('displays the milestone, label, author and assignee tokens', () => {
const tokens = findFilteredSearch().props('tokens'); const tokens = findFilteredSearch().props('tokens');
expect(tokens).toHaveLength(4); expect(tokens).toHaveLength(6);
expect(tokens[0].type).toBe(milestoneTokenType); [
expect(tokens[1].type).toBe(labelsTokenType); sourceBranchTokenType,
expect(tokens[2].type).toBe(authorTokenType); targetBranchTokenType,
expect(tokens[3].type).toBe(assigneeTokenType); milestoneTokenType,
labelsTokenType,
authorTokenType,
assigneeTokenType,
].forEach((tokenType, index) => {
expect(tokens[index].type).toBe(tokenType);
});
});
it('provides the initial source branch token', () => {
const { initialBranches } = getSearchToken(sourceBranchTokenType);
expect(initialBranches).toHaveLength(mockBranches.length);
});
it('provides the initial target branch token', () => {
const { initialBranches } = getSearchToken(targetBranchTokenType);
expect(initialBranches).toHaveLength(mockBranches.length);
}); });
it('provides the initial milestone token', () => { it('provides the initial milestone token', () => {
const { initialMilestones: milestoneToken } = getSearchToken(milestoneTokenType); const { initialMilestones } = getSearchToken(milestoneTokenType);
expect(milestoneToken).toHaveLength(filterMilestones.length); expect(initialMilestones).toHaveLength(filterMilestones.length);
}); });
it('provides the initial label token', () => { it('provides the initial label token', () => {
const { initialLabels: labelToken } = getSearchToken(labelsTokenType); const { initialLabels } = getSearchToken(labelsTokenType);
expect(labelToken).toHaveLength(filterLabels.length); expect(initialLabels).toHaveLength(filterLabels.length);
}); });
it('provides the initial author token', () => { it('provides the initial author token', () => {
const { initialAuthors: authorToken } = getSearchToken(authorTokenType); const { initialAuthors } = getSearchToken(authorTokenType);
expect(authorToken).toHaveLength(filterUsers.length); expect(initialAuthors).toHaveLength(filterUsers.length);
}); });
it('provides the initial assignee token', () => { it('provides the initial assignee token', () => {
const { initialAuthors: assigneeToken } = getSearchToken(assigneeTokenType); const { initialAuthors: initialAssignees } = getSearchToken(assigneeTokenType);
expect(assigneeToken).toHaveLength(filterUsers.length); expect(initialAssignees).toHaveLength(filterUsers.length);
}); });
}); });
...@@ -195,6 +225,14 @@ describe('Filter bar', () => { ...@@ -195,6 +225,14 @@ describe('Filter bar', () => {
it('clicks on the search button, setFilters is dispatched', () => { it('clicks on the search button, setFilters is dispatched', () => {
const filters = [ const filters = [
{
type: 'source_branch',
value: getFilterParams(mockBranches, { key: 'data', prop: 'name' })[2],
},
{
type: 'target_branch',
value: getFilterParams(mockBranches, { key: 'data', prop: 'name' })[0],
},
{ type: 'milestone', value: getFilterParams(filterMilestones, { key: 'data' })[2] }, { type: 'milestone', value: getFilterParams(filterMilestones, { key: 'data' })[2] },
{ type: 'labels', value: getFilterParams(filterLabels, { key: 'data' })[2] }, { type: 'labels', value: getFilterParams(filterLabels, { key: 'data' })[2] },
{ type: 'labels', value: getFilterParams(filterLabels, { key: 'data' })[4] }, { type: 'labels', value: getFilterParams(filterLabels, { key: 'data' })[4] },
...@@ -207,6 +245,8 @@ describe('Filter bar', () => { ...@@ -207,6 +245,8 @@ describe('Filter bar', () => {
expect(utils.processFilters).toHaveBeenCalledWith(filters); expect(utils.processFilters).toHaveBeenCalledWith(filters);
expect(setFiltersMock).toHaveBeenCalledWith(expect.anything(), { expect(setFiltersMock).toHaveBeenCalledWith(expect.anything(), {
selectedSourceBranch: selectedBranchParams[2],
selectedTargetBranch: selectedBranchParams[0],
selectedMilestone: selectedMilestoneParams[2], selectedMilestone: selectedMilestoneParams[2],
selectedLabelList: [selectedLabelParams[2], selectedLabelParams[4]], selectedLabelList: [selectedLabelParams[2], selectedLabelParams[4]],
selectedAssignee: selectedUserParams[2], selectedAssignee: selectedUserParams[2],
...@@ -216,13 +256,15 @@ describe('Filter bar', () => { ...@@ -216,13 +256,15 @@ describe('Filter bar', () => {
}); });
describe.each` describe.each`
stateKey | payload | paramKey | value stateKey | payload | paramKey | value
${'selectedMilestone'} | ${selectedMilestoneParams[3]} | ${'milestone_title'} | ${milestoneValues[3]} ${'selectedSourceBranch'} | ${selectedBranchParams[1]} | ${'source_branch_name'} | ${branchValues[1]}
${'selectedMilestone'} | ${selectedMilestoneParams[0]} | ${'milestone_title'} | ${milestoneValues[0]} ${'selectedTargetBranch'} | ${selectedBranchParams[2]} | ${'target_branch_name'} | ${branchValues[2]}
${'selectedLabelList'} | ${selectedLabelParams} | ${'label_name'} | ${labelValues} ${'selectedMilestone'} | ${selectedMilestoneParams[3]} | ${'milestone_title'} | ${milestoneValues[3]}
${'selectedLabelList'} | ${selectedLabelParams} | ${'label_name'} | ${labelValues} ${'selectedMilestone'} | ${selectedMilestoneParams[0]} | ${'milestone_title'} | ${milestoneValues[0]}
${'selectedAuthor'} | ${selectedUserParams[0]} | ${'author_username'} | ${userValues[0]} ${'selectedLabelList'} | ${selectedLabelParams} | ${'label_name'} | ${labelValues}
${'selectedAssignee'} | ${selectedUserParams[1]} | ${'assignee_username'} | ${userValues[1]} ${'selectedLabelList'} | ${selectedLabelParams} | ${'label_name'} | ${labelValues}
${'selectedAuthor'} | ${selectedUserParams[0]} | ${'author_username'} | ${userValues[0]}
${'selectedAssignee'} | ${selectedUserParams[1]} | ${'assignee_username'} | ${userValues[1]}
`( `(
'with a $stateKey updates the $paramKey url parameter', 'with a $stateKey updates the $paramKey url parameter',
({ stateKey, payload, paramKey, value }) => { ({ stateKey, payload, paramKey, value }) => {
......
...@@ -31,15 +31,15 @@ export const expectedMonthData = [ ...@@ -31,15 +31,15 @@ export const expectedMonthData = [
}, },
]; ];
export const throughputChartQuery = `query ($fullPath: ID!, $labels: [String!], $authorUsername: String, $assigneeUsername: String, $milestoneTitle: String) { export const throughputChartQuery = `query ($fullPath: ID!, $labels: [String!], $authorUsername: String, $assigneeUsername: String, $milestoneTitle: String, $sourceBranches: [String!], $targetBranches: [String!]) {
throughputChartData: project(fullPath: $fullPath) { throughputChartData: project(fullPath: $fullPath) {
May_2020: mergeRequests(first: 0, mergedBefore: "2020-06-01", mergedAfter: "2020-05-01", labels: $labels, authorUsername: $authorUsername, assigneeUsername: $assigneeUsername, milestoneTitle: $milestoneTitle) { May_2020: mergeRequests(first: 0, mergedBefore: "2020-06-01", mergedAfter: "2020-05-01", labels: $labels, authorUsername: $authorUsername, assigneeUsername: $assigneeUsername, milestoneTitle: $milestoneTitle, sourceBranches: $sourceBranches, targetBranches: $targetBranches) {
count count
} }
Jun_2020: mergeRequests(first: 0, mergedBefore: "2020-07-01", mergedAfter: "2020-06-01", labels: $labels, authorUsername: $authorUsername, assigneeUsername: $assigneeUsername, milestoneTitle: $milestoneTitle) { Jun_2020: mergeRequests(first: 0, mergedBefore: "2020-07-01", mergedAfter: "2020-06-01", labels: $labels, authorUsername: $authorUsername, assigneeUsername: $assigneeUsername, milestoneTitle: $milestoneTitle, sourceBranches: $sourceBranches, targetBranches: $targetBranches) {
count count
} }
Jul_2020: mergeRequests(first: 0, mergedBefore: "2020-08-01", mergedAfter: "2020-07-01", labels: $labels, authorUsername: $authorUsername, assigneeUsername: $assigneeUsername, milestoneTitle: $milestoneTitle) { Jul_2020: mergeRequests(first: 0, mergedBefore: "2020-08-01", mergedAfter: "2020-07-01", labels: $labels, authorUsername: $authorUsername, assigneeUsername: $assigneeUsername, milestoneTitle: $milestoneTitle, sourceBranches: $sourceBranches, targetBranches: $targetBranches) {
count count
} }
} }
......
...@@ -4,8 +4,10 @@ import testAction from 'helpers/vuex_action_helper'; ...@@ -4,8 +4,10 @@ import testAction from 'helpers/vuex_action_helper';
import * as actions from 'ee/analytics/shared/store/modules/filters/actions'; import * as actions from 'ee/analytics/shared/store/modules/filters/actions';
import * as types from 'ee/analytics/shared/store/modules/filters/mutation_types'; import * as types from 'ee/analytics/shared/store/modules/filters/mutation_types';
import initialState from 'ee/analytics/shared/store/modules/filters/state'; import initialState from 'ee/analytics/shared/store/modules/filters/state';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import Api from '~/api';
import { filterMilestones, filterUsers, filterLabels } from './mock_data'; import { filterMilestones, filterUsers, filterLabels } from './mock_data';
const milestonesEndpoint = 'fake_milestones_endpoint'; const milestonesEndpoint = 'fake_milestones_endpoint';
...@@ -113,6 +115,55 @@ describe('Filters actions', () => { ...@@ -113,6 +115,55 @@ describe('Filters actions', () => {
}); });
}); });
describe('fetchBranches', () => {
describe('success', () => {
beforeEach(() => {
const url = Api.buildUrl(Api.createBranchPath).replace(
':id',
encodeURIComponent(projectEndpoint),
);
mock.onGet(url).replyOnce(httpStatusCodes.OK, mockBranches);
});
it('dispatches RECEIVE_BRANCHES_SUCCESS with received data', () => {
return testAction(
actions.fetchBranches,
null,
{ ...state, projectEndpoint },
[
{ type: types.REQUEST_BRANCHES },
{ type: types.RECEIVE_BRANCHES_SUCCESS, payload: mockBranches },
],
[],
).then(({ data }) => {
expect(data).toBe(mockBranches);
});
});
});
describe('error', () => {
beforeEach(() => {
mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_BRANCHES_ERROR', () => {
return testAction(
actions.fetchBranches,
null,
state,
[
{ type: types.REQUEST_BRANCHES },
{
type: types.RECEIVE_BRANCHES_ERROR,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
},
],
[],
).then(() => expect(createFlash).toHaveBeenCalled());
});
});
});
describe('fetchAuthors', () => { describe('fetchAuthors', () => {
let restoreVersion; let restoreVersion;
beforeEach(() => { beforeEach(() => {
......
import { get } from 'lodash';
import initialState from 'ee/analytics/shared/store/modules/filters/state';
import mutations from 'ee/analytics/shared/store/modules/filters/mutations'; import mutations from 'ee/analytics/shared/store/modules/filters/mutations';
import * as types from 'ee/analytics/shared/store/modules/filters/mutation_types'; import * as types from 'ee/analytics/shared/store/modules/filters/mutation_types';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { filterMilestones, filterUsers, filterLabels } from './mock_data'; import { filterMilestones, filterUsers, filterLabels } from './mock_data';
let state = null; let state = null;
const branches = mockBranches.map(convertObjectPropsToCamelCase);
const milestones = filterMilestones.map(convertObjectPropsToCamelCase); const milestones = filterMilestones.map(convertObjectPropsToCamelCase);
const users = filterUsers.map(convertObjectPropsToCamelCase); const users = filterUsers.map(convertObjectPropsToCamelCase);
const labels = filterLabels.map(convertObjectPropsToCamelCase); const labels = filterLabels.map(convertObjectPropsToCamelCase);
...@@ -14,12 +18,7 @@ const filterValue = { value: 'foo' }; ...@@ -14,12 +18,7 @@ const filterValue = { value: 'foo' };
describe('Filters mutations', () => { describe('Filters mutations', () => {
const errorCode = 500; const errorCode = 500;
beforeEach(() => { beforeEach(() => {
state = { state = initialState();
authors: { selected: null, selectedList: [] },
milestones: { selected: null, selectedList: [] },
assignees: { selected: null, selectedList: [] },
labels: { selected: null, selectedList: [] },
};
}); });
afterEach(() => { afterEach(() => {
...@@ -38,34 +37,49 @@ describe('Filters mutations', () => { ...@@ -38,34 +37,49 @@ describe('Filters mutations', () => {
}); });
it.each` it.each`
mutation | stateKey | stateProp | filterName | value mutation | stateKey | filterName | value
${types.SET_SELECTED_FILTERS} | ${'authors'} | ${'selected'} | ${'selectedAuthor'} | ${null} ${types.SET_SELECTED_FILTERS} | ${'branches.source.selected'} | ${'selectedSourceBranch'} | ${null}
${types.SET_SELECTED_FILTERS} | ${'authors'} | ${'selected'} | ${'selectedAuthor'} | ${filterValue} ${types.SET_SELECTED_FILTERS} | ${'branches.source.selected'} | ${'selectedSourceBranch'} | ${filterValue}
${types.SET_SELECTED_FILTERS} | ${'authors'} | ${'selectedList'} | ${'selectedAuthorList'} | ${[]} ${types.SET_SELECTED_FILTERS} | ${'branches.source.selectedList'} | ${'selectedSourceBranchList'} | ${[]}
${types.SET_SELECTED_FILTERS} | ${'authors'} | ${'selectedList'} | ${'selectedAuthorList'} | ${[filterValue]} ${types.SET_SELECTED_FILTERS} | ${'branches.source.selectedList'} | ${'selectedSourceBranchList'} | ${[filterValue]}
${types.SET_SELECTED_FILTERS} | ${'milestones'} | ${'selected'} | ${'selectedMilestone'} | ${null} ${types.SET_SELECTED_FILTERS} | ${'branches.target.selected'} | ${'selectedTargetBranch'} | ${null}
${types.SET_SELECTED_FILTERS} | ${'milestones'} | ${'selected'} | ${'selectedMilestone'} | ${filterValue} ${types.SET_SELECTED_FILTERS} | ${'branches.target.selected'} | ${'selectedTargetBranch'} | ${filterValue}
${types.SET_SELECTED_FILTERS} | ${'milestones'} | ${'selectedList'} | ${'selectedMilestoneList'} | ${[]} ${types.SET_SELECTED_FILTERS} | ${'branches.target.selectedList'} | ${'selectedTargetBranchList'} | ${[]}
${types.SET_SELECTED_FILTERS} | ${'milestones'} | ${'selectedList'} | ${'selectedMilestoneList'} | ${[filterValue]} ${types.SET_SELECTED_FILTERS} | ${'branches.target.selectedList'} | ${'selectedTargetBranchList'} | ${[filterValue]}
${types.SET_SELECTED_FILTERS} | ${'assignees'} | ${'selected'} | ${'selectedAssignee'} | ${null} ${types.SET_SELECTED_FILTERS} | ${'authors.selected'} | ${'selectedAuthor'} | ${null}
${types.SET_SELECTED_FILTERS} | ${'assignees'} | ${'selected'} | ${'selectedAssignee'} | ${filterValue} ${types.SET_SELECTED_FILTERS} | ${'authors.selected'} | ${'selectedAuthor'} | ${filterValue}
${types.SET_SELECTED_FILTERS} | ${'assignees'} | ${'selectedList'} | ${'selectedAssigneeList'} | ${[]} ${types.SET_SELECTED_FILTERS} | ${'authors.selectedList'} | ${'selectedAuthorList'} | ${[]}
${types.SET_SELECTED_FILTERS} | ${'assignees'} | ${'selectedList'} | ${'selectedAssigneeList'} | ${[filterValue]} ${types.SET_SELECTED_FILTERS} | ${'authors.selectedList'} | ${'selectedAuthorList'} | ${[filterValue]}
${types.SET_SELECTED_FILTERS} | ${'labels'} | ${'selected'} | ${'selectedLabel'} | ${null} ${types.SET_SELECTED_FILTERS} | ${'milestones.selected'} | ${'selectedMilestone'} | ${null}
${types.SET_SELECTED_FILTERS} | ${'labels'} | ${'selected'} | ${'selectedLabel'} | ${filterValue} ${types.SET_SELECTED_FILTERS} | ${'milestones.selected'} | ${'selectedMilestone'} | ${filterValue}
${types.SET_SELECTED_FILTERS} | ${'labels'} | ${'selectedList'} | ${'selectedLabelList'} | ${[]} ${types.SET_SELECTED_FILTERS} | ${'milestones.selectedList'} | ${'selectedMilestoneList'} | ${[]}
${types.SET_SELECTED_FILTERS} | ${'labels'} | ${'selectedList'} | ${'selectedLabelList'} | ${[filterValue]} ${types.SET_SELECTED_FILTERS} | ${'milestones.selectedList'} | ${'selectedMilestoneList'} | ${[filterValue]}
${types.SET_SELECTED_FILTERS} | ${'assignees.selected'} | ${'selectedAssignee'} | ${null}
${types.SET_SELECTED_FILTERS} | ${'assignees.selected'} | ${'selectedAssignee'} | ${filterValue}
${types.SET_SELECTED_FILTERS} | ${'assignees.selectedList'} | ${'selectedAssigneeList'} | ${[]}
${types.SET_SELECTED_FILTERS} | ${'assignees.selectedList'} | ${'selectedAssigneeList'} | ${[filterValue]}
${types.SET_SELECTED_FILTERS} | ${'labels.selected'} | ${'selectedLabel'} | ${null}
${types.SET_SELECTED_FILTERS} | ${'labels.selected'} | ${'selectedLabel'} | ${filterValue}
${types.SET_SELECTED_FILTERS} | ${'labels.selectedList'} | ${'selectedLabelList'} | ${[]}
${types.SET_SELECTED_FILTERS} | ${'labels.selectedList'} | ${'selectedLabelList'} | ${[filterValue]}
`( `(
'$mutation will set $stateKey with a given value', '$mutation will set $stateKey with a given value',
({ mutation, stateKey, stateProp, filterName, value }) => { ({ mutation, stateKey, filterName, value }) => {
mutations[mutation](state, { [filterName]: value }); mutations[mutation](state, { [filterName]: value });
expect(state[stateKey][stateProp]).toEqual(value); expect(get(state, stateKey)).toEqual(value);
}, },
); );
it.each` it.each`
mutation | rootKey | stateKey | value mutation | rootKey | stateKey | value
${types.REQUEST_BRANCHES} | ${'branches'} | ${'isLoading'} | ${true}
${types.RECEIVE_BRANCHES_SUCCESS} | ${'branches'} | ${'isLoading'} | ${false}
${types.RECEIVE_BRANCHES_SUCCESS} | ${'branches'} | ${'data'} | ${branches}
${types.RECEIVE_BRANCHES_SUCCESS} | ${'branches'} | ${'errorCode'} | ${null}
${types.RECEIVE_BRANCHES_ERROR} | ${'branches'} | ${'isLoading'} | ${false}
${types.RECEIVE_BRANCHES_ERROR} | ${'branches'} | ${'data'} | ${[]}
${types.RECEIVE_BRANCHES_ERROR} | ${'branches'} | ${'errorCode'} | ${errorCode}
${types.REQUEST_MILESTONES} | ${'milestones'} | ${'isLoading'} | ${true} ${types.REQUEST_MILESTONES} | ${'milestones'} | ${'isLoading'} | ${true}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'isLoading'} | ${false} ${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'isLoading'} | ${false}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'data'} | ${milestones} ${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'data'} | ${milestones}
......
...@@ -10570,6 +10570,9 @@ msgstr "" ...@@ -10570,6 +10570,9 @@ msgstr ""
msgid "Failed to load authors. Please try again." msgid "Failed to load authors. Please try again."
msgstr "" msgstr ""
msgid "Failed to load branches. Please try again."
msgstr ""
msgid "Failed to load emoji list." msgid "Failed to load emoji list."
msgstr "" msgstr ""
...@@ -23833,6 +23836,9 @@ msgstr "" ...@@ -23833,6 +23836,9 @@ msgstr ""
msgid "Source (branch or tag)" msgid "Source (branch or tag)"
msgstr "" msgstr ""
msgid "Source Branch"
msgstr ""
msgid "Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}" msgid "Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}"
msgstr "" msgstr ""
...@@ -25394,6 +25400,9 @@ msgstr "" ...@@ -25394,6 +25400,9 @@ msgstr ""
msgid "There was a problem communicating with your device." msgid "There was a problem communicating with your device."
msgstr "" msgstr ""
msgid "There was a problem fetching branches."
msgstr ""
msgid "There was a problem fetching groups." msgid "There was a problem fetching groups."
msgstr "" msgstr ""
......
import { mockLabels } from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data'; import { mockLabels } from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
import Api from '~/api'; 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 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';
...@@ -33,6 +34,8 @@ export const mockAuthor3 = { ...@@ -33,6 +34,8 @@ export const mockAuthor3 = {
export const mockAuthors = [mockAuthor1, mockAuthor2, mockAuthor3]; export const mockAuthors = [mockAuthor1, mockAuthor2, mockAuthor3];
export const mockBranches = [{ name: 'Master' }, { name: 'v1.x' }, { name: 'my-Branch' }];
export const mockRegularMilestone = { export const mockRegularMilestone = {
id: 1, id: 1,
name: '4.0', name: '4.0',
...@@ -55,6 +58,16 @@ export const mockMilestones = [ ...@@ -55,6 +58,16 @@ export const mockMilestones = [
mockEscapedMilestone, mockEscapedMilestone,
]; ];
export const mockBranchToken = {
type: 'source_branch',
icon: 'branch',
title: 'Source Branch',
unique: true,
token: BranchToken,
operators: [{ value: '=', description: 'is', default: 'true' }],
fetchBranches: Api.branches.bind(Api),
};
export const mockAuthorToken = { export const mockAuthorToken = {
type: 'author_username', type: 'author_username',
icon: 'user', icon: 'user',
......
import { mount } from '@vue/test-utils';
import {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlFilteredSearchTokenSegment,
GlDropdownDivider,
} from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import {
DEFAULT_LABEL_NONE,
DEFAULT_LABEL_ANY,
} from '~/vue_shared/components/filtered_search_bar/constants';
import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
import { mockBranches, mockBranchToken } from '../mock_data';
jest.mock('~/flash');
const defaultStubs = {
Portal: true,
GlFilteredSearchSuggestionList: {
template: '<div></div>',
methods: {
getValue: () => '=',
},
},
};
function createComponent(options = {}) {
const {
config = mockBranchToken,
value = { data: '' },
active = false,
stubs = defaultStubs,
} = options;
return mount(BranchToken, {
propsData: {
config,
value,
active,
},
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
},
stubs,
});
}
describe('BranchToken', () => {
let mock;
let wrapper;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
wrapper.destroy();
});
describe('computed', () => {
beforeEach(async () => {
wrapper = createComponent({ value: { data: mockBranches[0].name } });
wrapper.setData({
branches: mockBranches,
});
await wrapper.vm.$nextTick();
});
describe('currentValue', () => {
it('returns lowercase string for `value.data`', () => {
expect(wrapper.vm.currentValue).toBe('master');
});
});
describe('activeBranch', () => {
it('returns object for currently present `value.data`', () => {
expect(wrapper.vm.activeBranch).toEqual(mockBranches[0]);
});
});
});
describe('methods', () => {
beforeEach(() => {
wrapper = createComponent();
});
describe('fetchBranchBySearchTerm', () => {
it('calls `config.fetchBranches` with provided searchTerm param', () => {
jest.spyOn(wrapper.vm.config, 'fetchBranches');
wrapper.vm.fetchBranchBySearchTerm('foo');
expect(wrapper.vm.config.fetchBranches).toHaveBeenCalledWith('foo');
});
it('sets response to `branches` when request is succesful', () => {
jest.spyOn(wrapper.vm.config, 'fetchBranches').mockResolvedValue({ data: mockBranches });
wrapper.vm.fetchBranchBySearchTerm('foo');
return waitForPromises().then(() => {
expect(wrapper.vm.branches).toEqual(mockBranches);
});
});
it('calls `createFlash` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({});
wrapper.vm.fetchBranchBySearchTerm('foo');
return waitForPromises().then(() => {
expect(createFlash).toHaveBeenCalledWith({
message: 'There was a problem fetching branches.',
});
});
});
it('sets `loading` to false when request completes', () => {
jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({});
wrapper.vm.fetchBranchBySearchTerm('foo');
return waitForPromises().then(() => {
expect(wrapper.vm.loading).toBe(false);
});
});
});
});
describe('template', () => {
const defaultBranches = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
async function showSuggestions() {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await wrapper.vm.$nextTick();
}
beforeEach(async () => {
wrapper = createComponent({ value: { data: mockBranches[0].name } });
wrapper.setData({
branches: mockBranches,
});
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(mockBranches[0].name);
});
it('renders provided defaultBranches as suggestions', async () => {
wrapper = createComponent({
active: true,
config: { ...mockBranchToken, defaultBranches },
stubs: { Portal: true },
});
await showSuggestions();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(defaultBranches.length);
defaultBranches.forEach((branch, index) => {
expect(suggestions.at(index).text()).toBe(branch.text);
});
});
it('does not render divider when no defaultBranches', async () => {
wrapper = createComponent({
active: true,
config: { ...mockBranchToken, defaultBranches: [] },
stubs: { Portal: true },
});
await showSuggestions();
expect(wrapper.contains(GlFilteredSearchSuggestion)).toBe(false);
expect(wrapper.contains(GlDropdownDivider)).toBe(false);
});
it('renders no suggestions as default', async () => {
wrapper = createComponent({
active: true,
config: { ...mockBranchToken },
stubs: { Portal: true },
});
await showSuggestions();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(0);
});
});
});
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