Commit 47699481 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '235367-migrate-board-scope-milestone-dropdown-to-gldropdown' into 'master'

Refactor board scope milestone dropdown

See merge request gitlab-org/gitlab!66896
parents 5b6ad9d5 d88a3e5a
......@@ -18,7 +18,7 @@ const boardDefaults = {
id: false,
name: '',
labels: [],
milestone_id: undefined,
milestone: {},
iteration_id: undefined,
assignee: {},
weight: null,
......@@ -190,10 +190,9 @@ export default {
return {
weight: this.board.weight,
assigneeId: this.board.assignee?.id || null,
milestoneId:
this.board.milestone?.id || this.board.milestone?.id === 0
? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
: null,
milestoneId: this.board.milestone?.id
? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
: null,
iterationId: this.board.iteration_id
? convertToGraphQLId(TYPE_ITERATION, this.board.iteration_id)
: null,
......@@ -304,9 +303,14 @@ export default {
});
},
setAssignee(assigneeId) {
this.board.assignee = {
this.$set(this.board, 'assignee', {
id: assigneeId,
};
});
},
setMilestone(milestoneId) {
this.$set(this.board, 'milestone', {
id: milestoneId,
});
},
},
};
......@@ -376,6 +380,7 @@ export default {
@set-iteration="setIteration"
@set-board-labels="setBoardLabels"
@set-assignee="setAssignee"
@set-milestone="setMilestone"
/>
</form>
</gl-modal>
......
import { isArray } from 'lodash';
/**
* Ids generated by GraphQL endpoints are usually in the format
* gid://gitlab/Environments/123. This method checks if the passed id follows that format
*
* @param {String|Number} id The id value
* @returns {Boolean}
*/
export const isGid = (id) => {
if (typeof id === 'string' && id.startsWith('gid://gitlab/')) {
return true;
}
return false;
};
/**
* Ids generated by GraphQL endpoints are usually in the format
* gid://gitlab/Environments/123. This method extracts Id number
......@@ -35,6 +50,10 @@ export const convertToGraphQLId = (type, id) => {
throw new TypeError(`id must be a number or string; got ${typeof id}`);
}
if (isGid(id)) {
return id;
}
return `gid://gitlab/${type}/${id}`;
};
......
#import "./milestone.fragment.graphql"
query groupMilestones($fullPath: ID!, $title: String, $state: MilestoneStateEnum) {
workspace: group(fullPath: $fullPath) {
__typename
id
attributes: milestones(
searchTitle: $title
state: $state
sort: EXPIRED_LAST_DUE_DATE_ASC
first: 20
includeAncestors: true
) {
nodes {
...MilestoneFragment
state
}
}
}
}
/* eslint-disable @gitlab/require-i18n-strings */
import { __ } from '~/locale';
import DropdownWidget from './dropdown_widget.vue';
export default {
component: DropdownWidget,
title: 'vue_shared/components/dropdown/dropdown_widget/dropdown_widget',
};
const Template = (args, { argTypes }) => ({
components: { DropdownWidget },
props: Object.keys(argTypes),
template: '<dropdown-widget v-bind="$props" v-on="$props" />',
});
export const Default = Template.bind({});
Default.args = {
options: [
{ id: 'gid://gitlab/Milestone/-1', title: __('Any Milestone') },
{ id: 'gid://gitlab/Milestone/0', title: __('No Milestone') },
{ id: 'gid://gitlab/Milestone/-2', title: __('Upcoming') },
{ id: 'gid://gitlab/Milestone/-3', title: __('Started') },
],
selectText: 'Select',
searchText: 'Search',
};
<script>
import {
GlLoadingIcon,
GlDropdown,
GlDropdownForm,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByType,
} from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlLoadingIcon,
GlDropdown,
GlDropdownForm,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByType,
},
props: {
selectText: {
type: String,
required: false,
default: __('Select'),
},
searchText: {
type: String,
required: false,
default: __('Search'),
},
presetOptions: {
type: Array,
required: false,
default: () => [],
},
options: {
type: Array,
required: false,
default: () => [],
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
selected: {
type: Object,
required: false,
default: () => {},
},
searchTerm: {
type: String,
required: false,
default: '',
},
},
computed: {
isSearchEmpty() {
return this.searchTerm === '' && !this.isLoading;
},
noOptionsFound() {
return !this.isSearchEmpty && this.options.length === 0;
},
},
methods: {
selectOption(option) {
this.$emit('set-option', option || null);
},
isSelected(option) {
return this.selected && this.selected.title === option.title;
},
showDropdown() {
this.$refs.dropdown.show();
},
setFocus() {
this.$refs.search.focusInput();
},
setSearchTerm(search) {
this.$emit('set-search', search);
},
},
i18n: {
noMatchingResults: __('No matching results'),
},
};
</script>
<template>
<gl-dropdown
ref="dropdown"
:text="selectText"
lazy
menu-class="gl-w-full!"
class="gl-w-full"
v-on="$listeners"
@shown="setFocus"
>
<template #header>
<gl-search-box-by-type
ref="search"
:value="searchTerm"
:placeholder="searchText"
class="js-dropdown-input-field"
@input="setSearchTerm"
/>
</template>
<gl-dropdown-form class="gl-relative gl-min-h-7">
<gl-loading-icon
v-if="isLoading"
size="md"
class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
/>
<template v-else>
<template v-if="isSearchEmpty && presetOptions.length > 0">
<gl-dropdown-item
v-for="option in presetOptions"
:key="option.id"
:is-checked="isSelected(option)"
:is-check-centered="true"
:is-check-item="true"
@click="selectOption(option)"
>
{{ option.title }}
</gl-dropdown-item>
<gl-dropdown-divider />
</template>
<gl-dropdown-item
v-for="option in options"
:key="option.id"
:is-checked="isSelected(option)"
:is-check-centered="true"
:is-check-item="true"
data-testid="unselected-option"
@click="selectOption(option)"
>
{{ option.title }}
</gl-dropdown-item>
<gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
{{ $options.i18n.noMatchingResults }}
</gl-dropdown-item>
</template>
</gl-dropdown-form>
<template #footer>
<slot name="footer"></slot>
</template>
</gl-dropdown>
</template>
......@@ -101,6 +101,7 @@ export default {
:group-id="groupId"
:project-id="projectId"
:can-edit="canAdminBoard"
@set-milestone="$emit('set-milestone', $event)"
/>
<board-scope-current-iteration
......
<script>
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import MilestoneSelect from '~/milestone_select';
import { GlButton } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapActions, mapGetters } from 'vuex';
import { s__ } from '~/locale';
const ANY_MILESTONE = 'Any milestone';
const NO_MILESTONE = 'No milestone';
import groupMilestonesQuery from '~/sidebar/queries/group_milestones.query.graphql';
import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
import { MilestonesPreset, ANY_MILESTONE } from '../constants';
export default {
MilestonesPreset,
components: {
GlLoadingIcon,
GlIcon,
GlButton,
DropdownWidget,
},
inject: ['fullPath'],
props: {
board: {
type: Object,
......@@ -31,48 +39,93 @@ export default {
default: false,
},
},
computed: {
milestoneTitle() {
if (this.noMilestone) return NO_MILESTONE;
return this.board.milestone ? this.board.milestone.title : ANY_MILESTONE;
data() {
return {
search: '',
milestones: [],
selected: this.board.milestone,
isEditing: false,
isDropdownShowing: false,
};
},
apollo: {
milestones: {
query() {
return this.isProjectBoard ? projectMilestonesQuery : groupMilestonesQuery;
},
variables() {
return {
fullPath: this.fullPath,
title: this.search,
first: 20,
};
},
skip() {
return !this.isEditing;
},
update(data) {
return data?.workspace?.attributes?.nodes || [];
},
error() {
this.setError({ message: this.$options.i18n.errorSearchingMilestones });
},
},
noMilestone() {
return this.milestoneId === 0;
},
computed: {
...mapGetters(['isProjectBoard']),
anyMilestone() {
return this.selected.title === ANY_MILESTONE.title;
},
milestoneId() {
return this.board.milestone_id;
milestoneTitle() {
return this.selected.title;
},
milestoneTitleClass() {
return this.milestoneTitle === ANY_MILESTONE ? 'text-secondary' : 'bold';
return this.anyMilestone ? 'gl-text-gray-500' : 'gl-font-weight-bold';
},
selected() {
if (this.noMilestone) return NO_MILESTONE;
return this.board.milestone ? this.board.milestone.name : '';
isLoading() {
return this.$apollo.queries.milestones.loading;
},
},
mounted() {
this.milestoneDropdown = new MilestoneSelect(null, this.$refs.dropdownButton, {
handleClick: this.selectMilestone,
});
created() {
if (isEmpty(this.board.milestone)) {
this.selected = ANY_MILESTONE;
}
},
methods: {
...mapActions(['setError']),
selectMilestone(milestone) {
let { id } = milestone;
// swap the IDs of 'Any' and 'No' milestone to what backend requires
if (milestone.title === ANY_MILESTONE) {
id = -1;
} else if (milestone.title === NO_MILESTONE) {
id = 0;
this.selected = milestone;
this.toggleEdit();
this.$emit('set-milestone', milestone?.id || null);
},
toggleEdit() {
if (!this.isEditing && !this.isDropdownShowing) {
this.isEditing = true;
this.showDropdown();
} else {
this.isEditing = false;
this.isDropdownShowing = false;
}
// eslint-disable-next-line vue/no-mutating-props
this.board.milestone_id = id;
// eslint-disable-next-line vue/no-mutating-props
this.board.milestone = {
...milestone,
id,
};
},
showDropdown() {
this.$refs.editDropdown.showDropdown();
this.isDropdownShowing = true;
},
hideDropdown() {
this.isEditing = false;
},
setSearch(search) {
this.search = search;
},
},
i18n: {
label: s__('BoardScope|Milestone'),
errorSearchingMilestones: s__(
'BoardScope|An error occurred while getting milestones, please try again.',
),
searchMilestones: s__('BoardScope|Search milestones'),
selectMilestone: s__('BoardScope|Select milestone'),
edit: s__('BoardScope|Edit'),
},
};
</script>
......@@ -80,60 +133,33 @@ export default {
<template>
<div class="block milestone">
<div class="title gl-mb-3">
{{ __('Milestone') }}
<button v-if="canEdit" type="button" class="edit-link btn btn-blank float-right">
{{ __('Edit') }}
</button>
{{ $options.i18n.label }}
<gl-button
v-if="canEdit"
variant="link"
class="edit-link float-right gl-text-gray-900!"
@click="toggleEdit"
>
{{ $options.i18n.edit }}
</gl-button>
</div>
<div :class="milestoneTitleClass" class="value">{{ milestoneTitle }}</div>
<div class="selectbox" style="display: none">
<input :value="milestoneId" name="milestone_id" type="hidden" />
<div class="dropdown">
<!-- eslint-disable @gitlab/vue-no-data-toggle -->
<button
ref="dropdownButton"
:data-selected="selected"
:data-project-id="projectId"
:data-group-id="groupId"
:data-show-no="true"
:data-show-any="true"
:data-show-started="true"
:data-show-upcoming="true"
:data-use-id="true"
class="dropdown-menu-toggle wide"
data-toggle="dropdown"
type="button"
>
{{ __('Milestone') }}
<gl-icon
name="chevron-down"
class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
:size="16"
/>
</button>
<!-- eslint-enable @gitlab/vue-no-data-toggle -->
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
<div class="dropdown-input">
<input
type="search"
class="dropdown-input-field"
:placeholder="__('Search milestones')"
autocomplete="off"
/>
<gl-icon
name="search"
class="dropdown-input-search gl-absolute gl-top-3 gl-right-5 gl-text-gray-300 gl-pointer-events-none"
/>
<gl-icon
name="close"
class="dropdown-input-clear js-dropdown-input-clear gl-right-5 gl-absolute gl-top-3 gl-text-gray-500"
/>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading"><gl-loading-icon size="sm" /></div>
</div>
</div>
<div v-if="!isEditing" :class="milestoneTitleClass" data-testid="selected-milestone">
{{ milestoneTitle }}
</div>
<dropdown-widget
v-show="isEditing"
ref="editDropdown"
:select-text="$options.i18n.selectMilestone"
:search-text="$options.i18n.searchMilestones"
:preset-options="$options.MilestonesPreset"
:options="milestones"
:is-loading="isLoading"
:selected="selected"
:search-term="search"
@hide="hideDropdown"
@set-option="selectMilestone"
@set-search="setSearch"
/>
</div>
</template>
......@@ -55,6 +55,30 @@ export const MilestoneIDs = {
NONE: 0,
};
export const ANY_MILESTONE = {
id: 'gid://gitlab/Milestone/-1',
title: s__('BoardScope|Any Milestone'),
};
export const NO_MILESTONE = {
id: 'gid://gitlab/Milestone/0',
title: s__('BoardScope|No milestone'),
};
export const UPCOMING_MILESTONE = {
id: 'gid://gitlab/Milestone/-2',
title: s__('BoardScope|Upcoming'),
};
export const STARTED_MILESTONE = {
id: 'gid://gitlab/Milestone/-3',
title: s__('BoardScope|Started'),
};
export const MilestonesPreset = [
ANY_MILESTONE,
NO_MILESTONE,
UPCOMING_MILESTONE,
STARTED_MILESTONE,
];
export const WeightFilterType = {
none: 'None',
};
......
......@@ -65,8 +65,8 @@ RSpec.describe 'Scoped issue boards', :js do
expect(page).to have_selector('.board-card', count: 2)
end
it 'creates board filtering by Any milestone' do
create_board_milestone('Any milestone')
it 'creates board filtering by Any Milestone' do
create_board_milestone('Any Milestone')
expect(find('.tokens-container')).to have_content("")
expect(page).to have_selector('.board-card', count: 3)
......@@ -228,7 +228,7 @@ RSpec.describe 'Scoped issue boards', :js do
edit_board.click
expect(find('.milestone .value')).to have_content(milestone.title)
expect(find('[data-testid="selected-milestone"]')).to have_content(milestone.title)
expect(find('[data-testid="selected-assignee"]')).to have_content(user.name)
expect(find('.weight .value')).to have_content(2)
end
......@@ -242,7 +242,7 @@ RSpec.describe 'Scoped issue boards', :js do
end
it 'sets board to any milestone' do
update_board_milestone('Any milestone')
update_board_milestone('Any Milestone')
expect(find('.tokens-container')).not_to have_content(milestone.title)
......
......@@ -37,6 +37,7 @@ describe('BoardScope', () => {
},
stubs: {
AssigneeSelect: true,
BoardMilestoneSelect: true,
},
});
}
......
import Vue from 'vue';
import { GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import MilestoneSelect from 'ee/boards/components/milestone_select.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { boardObj } from 'jest/boards/mock_data';
import Api from '~/api';
import IssuableContext from '~/issuable_context';
let vm;
function selectedText() {
return vm.$el.querySelector('.value').innerText.trim();
}
function activeDropdownItem(index) {
const items = vm.$el.querySelectorAll('.is-active');
if (!items[index]) return '';
return items[index].innerText.trim();
}
const milestone = {
id: 1,
title: 'first milestone',
name: 'first milestone',
due_date: '2015-05-05',
expired: true,
};
const milestone2 = {
id: 2,
title: 'second milestone',
name: 'second milestone',
due_date: null,
expired: false,
};
import { mockProjectMilestonesResponse, mockGroupMilestonesResponse } from 'jest/sidebar/mock_data';
describe('Milestone select component', () => {
beforeEach((done) => {
setFixtures('<div class="test-container"></div>');
import defaultStore from '~/boards/stores';
import groupMilestonesQuery from '~/sidebar/queries/group_milestones.query.graphql';
import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
// eslint-disable-next-line no-new
new IssuableContext();
const localVue = createLocalVue();
localVue.use(VueApollo);
const Component = Vue.extend(MilestoneSelect);
vm = new Component({
describe('Milestone select component', () => {
let wrapper;
let fakeApollo;
let store;
const selectedText = () => wrapper.find('[data-testid="selected-milestone"]').text();
const findEditButton = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(DropdownWidget);
const milestonesQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectMilestonesResponse);
const groupUsersQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupMilestonesResponse);
const createStore = ({ isGroupBoard = false, isProjectBoard = false } = {}) => {
store = new Vuex.Store({
...defaultStore,
getters: {
isGroupBoard: () => isGroupBoard,
isProjectBoard: () => isProjectBoard,
},
});
};
const createComponent = ({
props = {},
milestonesQueryHandler = milestonesQueryHandlerSuccess,
} = {}) => {
fakeApollo = createMockApollo([
[projectMilestonesQuery, milestonesQueryHandler],
[groupMilestonesQuery, groupUsersQueryHandlerSuccess],
]);
wrapper = shallowMount(MilestoneSelect, {
localVue,
store,
apolloProvider: fakeApollo,
propsData: {
board: boardObj,
groupId: 2,
projectId: 2,
canEdit: true,
...props,
},
provide: {
fullPath: 'gitlab-org',
},
stubs: {
GlDropdown,
GlDropdownItem,
},
}).$mount('.test-container');
});
setImmediate(done);
});
// We need to mock out `showDropdown` which
// invokes `show` method of BDropdown used inside GlDropdown.
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
};
describe('canEdit', () => {
it('hides Edit button', (done) => {
vm.canEdit = false;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.edit-link')).toBeFalsy();
done();
});
});
beforeEach(() => {
createStore({ isProjectBoard: true });
createComponent();
});
it('shows Edit button if true', (done) => {
vm.canEdit = true;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.edit-link')).toBeTruthy();
done();
});
});
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
store = null;
});
describe('selected value', () => {
describe('when not editing', () => {
it('defaults to Any milestone', () => {
expect(selectedText()).toContain('Any milestone');
expect(selectedText()).toContain('Any Milestone');
});
it('shows No milestone', (done) => {
vm.board.milestone_id = 0;
Vue.nextTick(() => {
expect(selectedText()).toContain('No milestone');
done();
});
it('skips the queries and does not render dropdown', () => {
expect(milestonesQueryHandlerSuccess).not.toHaveBeenCalled();
expect(findDropdown().isVisible()).toBe(false);
});
});
it('shows selected milestone title', (done) => {
vm.board.milestone_id = 20;
vm.board.milestone = {
id: 20,
title: 'Selected milestone',
};
Vue.nextTick(() => {
expect(selectedText()).toContain('Selected milestone');
done();
});
});
describe('clicking dropdown items', () => {
beforeEach(() => {
jest.spyOn(Api, 'projectMilestones').mockResolvedValue({ data: [milestone, milestone2] });
});
it('sets Any milestone', async (done) => {
vm.board.milestone_id = 0;
vm.$el.querySelector('.edit-link').click();
await vm.$nextTick();
jest.runOnlyPendingTimers();
setImmediate(() => {
vm.$el.querySelectorAll('li a')[0].click();
});
describe('when editing', () => {
it('trigger query and renders dropdown with passed milestones', async () => {
findEditButton().vm.$emit('click');
await waitForPromises();
await nextTick();
expect(milestonesQueryHandlerSuccess).toHaveBeenCalled();
setImmediate(() => {
expect(activeDropdownItem(0)).toEqual('Any milestone');
expect(selectedText()).toEqual('Any milestone');
done();
});
});
expect(findDropdown().isVisible()).toBe(true);
expect(findDropdown().props('options')).toHaveLength(2);
});
});
it('sets No milestone', (done) => {
vm.$el.querySelector('.edit-link').click();
describe('canEdit', () => {
it('hides Edit button', async () => {
wrapper.setProps({ canEdit: false });
await nextTick();
jest.runOnlyPendingTimers();
expect(findEditButton().exists()).toBe(false);
});
setImmediate(() => {
vm.$el.querySelectorAll('li a')[1].click();
});
it('shows Edit button if true', () => {
expect(findEditButton().exists()).toBe(true);
});
});
setImmediate(() => {
expect(activeDropdownItem(0)).toEqual('No milestone');
expect(selectedText()).toEqual('No milestone');
done();
});
it.each`
boardType | mockedResponse | queryHandler | notCalledHandler
${'group'} | ${mockGroupMilestonesResponse} | ${groupUsersQueryHandlerSuccess} | ${milestonesQueryHandlerSuccess}
${'project'} | ${mockProjectMilestonesResponse} | ${milestonesQueryHandlerSuccess} | ${groupUsersQueryHandlerSuccess}
`(
'fetches $boardType milestones',
async ({ boardType, mockedResponse, queryHandler, notCalledHandler }) => {
createStore({ isProjectBoard: boardType === 'project', isGroupBoard: boardType === 'group' });
createComponent({
[queryHandler]: jest.fn().mockResolvedValue(mockedResponse),
});
it('sets milestone', (done) => {
vm.$el.querySelector('.edit-link').click();
jest.runOnlyPendingTimers();
findEditButton().vm.$emit('click');
await waitForPromises();
await nextTick();
setImmediate(() => {
vm.$el.querySelectorAll('li a')[4].click();
});
setImmediate(() => {
// "second milestone" is not expired, hence it shows up to the top.
expect(activeDropdownItem(0)).toBe('second milestone');
expect(selectedText()).toBe('second milestone');
expect(vm.board.milestone).toEqual(milestone2);
done();
});
});
});
});
expect(queryHandler).toHaveBeenCalled();
expect(notCalledHandler).not.toHaveBeenCalled();
},
);
});
......@@ -3891,6 +3891,9 @@ msgstr ""
msgid "Any Author"
msgstr ""
msgid "Any Milestone"
msgstr ""
msgid "Any branch"
msgstr ""
......@@ -5309,9 +5312,15 @@ msgstr ""
msgid "BoardNewIssue|Select a project"
msgstr ""
msgid "BoardScope|An error occurred while getting milestones, please try again."
msgstr ""
msgid "BoardScope|An error occurred while searching for users, please try again."
msgstr ""
msgid "BoardScope|Any Milestone"
msgstr ""
msgid "BoardScope|Any assignee"
msgstr ""
......@@ -5321,12 +5330,30 @@ msgstr ""
msgid "BoardScope|Edit"
msgstr ""
msgid "BoardScope|Milestone"
msgstr ""
msgid "BoardScope|No matching results"
msgstr ""
msgid "BoardScope|No milestone"
msgstr ""
msgid "BoardScope|Search milestones"
msgstr ""
msgid "BoardScope|Select assignee"
msgstr ""
msgid "BoardScope|Select milestone"
msgstr ""
msgid "BoardScope|Started"
msgstr ""
msgid "BoardScope|Upcoming"
msgstr ""
msgid "Boards"
msgstr ""
......@@ -22169,6 +22196,9 @@ msgstr ""
msgid "No Matching Results"
msgstr ""
msgid "No Milestone"
msgstr ""
msgid "No Scopes"
msgstr ""
......
import {
isGid,
getIdFromGraphQLId,
convertToGraphQLId,
convertToGraphQLIds,
......@@ -10,6 +11,16 @@ const mockType = 'Group';
const mockId = 12;
const mockGid = `gid://gitlab/Group/12`;
describe('isGid', () => {
it('returns true if passed id is gid', () => {
expect(isGid(mockGid)).toBe(true);
});
it('returns false if passed id is not gid', () => {
expect(isGid(mockId)).toBe(false);
});
});
describe('getIdFromGraphQLId', () => {
[
{
......@@ -67,6 +78,10 @@ describe('convertToGraphQLId', () => {
`('throws TypeError with "$message" if a param is missing', ({ type, id, message }) => {
expect(() => convertToGraphQLId(type, id)).toThrow(new TypeError(message));
});
it('returns id as is if it follows the gid format', () => {
expect(convertToGraphQLId(mockType, mockGid)).toStrictEqual(mockGid);
});
});
describe('convertToGraphQLIds', () => {
......
......@@ -585,6 +585,19 @@ export const mockProjectMilestonesResponse = {
},
};
export const mockGroupMilestonesResponse = {
data: {
workspace: {
id: 'gid://gitlab/Group/1',
attributes: {
nodes: [mockMilestone1, mockMilestone2],
},
__typename: 'MilestoneConnection',
},
__typename: 'Group',
},
};
export const noCurrentMilestoneResponse = {
data: {
workspace: {
......
import { GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
describe('DropdownWidget component', () => {
let wrapper;
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findSearch = () => wrapper.findComponent(GlSearchBoxByType);
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(DropdownWidget, {
propsData: {
...props,
options: [
{
id: '1',
title: 'Option 1',
},
{
id: '2',
title: 'Option 2',
},
],
},
stubs: {
GlDropdown,
},
});
// We need to mock out `showDropdown` which
// invokes `show` method of BDropdown used inside GlDropdown.
// Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54895#note_524281679
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('passes default selectText prop to dropdown', () => {
expect(findDropdown().props('text')).toBe('Select');
});
describe('when dropdown is open', () => {
beforeEach(async () => {
findDropdown().vm.$emit('show');
await wrapper.vm.$nextTick();
});
it('emits search event when typing in search box', () => {
const searchTerm = 'searchTerm';
findSearch().vm.$emit('input', searchTerm);
expect(wrapper.emitted('set-search')).toEqual([[searchTerm]]);
});
it('renders one selectable item per passed option', async () => {
expect(findDropdownItems()).toHaveLength(2);
});
it('emits set-option event when clicking on an option', async () => {
wrapper
.findAll('[data-testid="unselected-option"]')
.at(1)
.vm.$emit('click', new Event('click'));
await wrapper.vm.$nextTick();
expect(wrapper.emitted('set-option')).toEqual([[wrapper.props().options[1]]]);
});
});
});
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