Commit f1bdac24 authored by Markus Koller's avatar Markus Koller

Merge branch 'move-timebox-reports-graphql' into 'master'

Generalize burnup chart service for other metrics

See merge request gitlab-org/gitlab!45121
parents 8c801481 47c780c0
...@@ -6,13 +6,13 @@ import { ...@@ -6,13 +6,13 @@ import {
GlDatepicker, GlDatepicker,
GlLink, GlLink,
GlSprintf, GlSprintf,
GlSearchBoxByType,
GlButton, GlButton,
GlFormInput, GlFormInput,
} from '@gitlab/ui'; } from '@gitlab/ui';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import Api from '~/api'; import Api from '~/api';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
export default { export default {
name: 'InviteMembersModal', name: 'InviteMembersModal',
...@@ -23,9 +23,9 @@ export default { ...@@ -23,9 +23,9 @@ export default {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlSprintf, GlSprintf,
GlSearchBoxByType,
GlButton, GlButton,
GlFormInput, GlFormInput,
MembersTokenSelect,
}, },
props: { props: {
groupId: { groupId: {
...@@ -129,44 +129,45 @@ export default { ...@@ -129,44 +129,45 @@ export default {
}, },
labels: { labels: {
modalTitle: s__('InviteMembersModal|Invite team members'), modalTitle: s__('InviteMembersModal|Invite team members'),
userToInvite: s__('InviteMembersModal|GitLab member or Email address'), newUsersToInvite: s__('InviteMembersModal|GitLab member or Email address'),
userPlaceholder: s__('InviteMembersModal|Search for members to invite'), userPlaceholder: s__('InviteMembersModal|Search for members to invite'),
accessLevel: s__('InviteMembersModal|Choose a role permission'), accessLevel: s__('InviteMembersModal|Choose a role permission'),
accessExpireDate: s__('InviteMembersModal|Access expiration date (optional)'), accessExpireDate: s__('InviteMembersModal|Access expiration date (optional)'),
toastMessageSuccessful: s__('InviteMembersModal|Users were succesfully added'), toastMessageSuccessful: s__('InviteMembersModal|Members were successfully added'),
toastMessageUnsuccessful: s__('InviteMembersModal|User not invited. Feature coming soon!'), toastMessageUnsuccessful: s__('InviteMembersModal|Some of the members could not be added'),
readMoreText: s__(`InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`), readMoreText: s__(`InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`),
inviteButtonText: s__('InviteMembersModal|Invite'), inviteButtonText: s__('InviteMembersModal|Invite'),
cancelButtonText: s__('InviteMembersModal|Cancel'), cancelButtonText: s__('InviteMembersModal|Cancel'),
headerCloseLabel: s__('InviteMembersModal|Close invite team members'),
}, },
membersTokenSelectLabelId: 'invite-members-input',
}; };
</script> </script>
<template> <template>
<gl-modal :modal-id="modalId" size="sm" :title="$options.labels.modalTitle"> <gl-modal
:modal-id="modalId"
size="sm"
:title="$options.labels.modalTitle"
:header-close-label="$options.labels.headerCloseLabel"
>
<div class="gl-ml-5 gl-mr-5"> <div class="gl-ml-5 gl-mr-5">
<div>{{ introText }}</div> <div>{{ introText }}</div>
<label class="gl-font-weight-bold gl-mt-5">{{ $options.labels.userToInvite }}</label> <label :id="$options.membersTokenSelectLabelId" class="gl-font-weight-bold gl-mt-5">{{
$options.labels.newUsersToInvite
}}</label>
<div class="gl-mt-2"> <div class="gl-mt-2">
<gl-search-box-by-type <members-token-select
v-model="newUsersToInvite" v-model="newUsersToInvite"
:label="$options.labels.newUsersToInvite"
:aria-labelledby="$options.membersTokenSelectLabelId"
:placeholder="$options.labels.userPlaceholder" :placeholder="$options.labels.userPlaceholder"
type="text"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/> />
</div> </div>
<label class="gl-font-weight-bold gl-mt-5">{{ $options.labels.accessLevel }}</label> <label class="gl-font-weight-bold gl-mt-5">{{ $options.labels.accessLevel }}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full"> <div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-dropdown <gl-dropdown class="gl-shadow-none gl-w-full" v-bind="$attrs" :text="selectedRoleName">
menu-class="dropdown-menu-selectable"
class="gl-shadow-none gl-w-full"
v-bind="$attrs"
:text="selectedRoleName"
>
<template v-for="(key, item) in accessLevels"> <template v-for="(key, item) in accessLevels">
<gl-dropdown-item <gl-dropdown-item
:key="key" :key="key"
...@@ -215,9 +216,13 @@ export default { ...@@ -215,9 +216,13 @@ export default {
{{ $options.labels.cancelButtonText }} {{ $options.labels.cancelButtonText }}
</gl-button> </gl-button>
<div class="gl-mr-3"></div> <div class="gl-mr-3"></div>
<gl-button ref="inviteButton" variant="success" @click="sendInvite">{{ <gl-button
$options.labels.inviteButtonText ref="inviteButton"
}}</gl-button> :disabled="!newUsersToInvite"
variant="success"
@click="sendInvite"
>{{ $options.labels.inviteButtonText }}</gl-button
>
</div> </div>
</template> </template>
</gl-modal> </gl-modal>
......
<script>
import { debounce } from 'lodash';
import { GlTokenSelector, GlAvatar, GlAvatarLabeled } from '@gitlab/ui';
import { USER_SEARCH_DELAY } from '../constants';
import Api from '~/api';
export default {
components: {
GlTokenSelector,
GlAvatar,
GlAvatarLabeled,
},
props: {
placeholder: {
type: String,
required: false,
default: '',
},
ariaLabelledby: {
type: String,
required: true,
},
},
data() {
return {
loading: false,
query: '',
users: [],
selectedTokens: [],
hasBeenFocused: false,
hideDropdownWithNoItems: true,
};
},
computed: {
newUsersToInvite() {
return this.selectedTokens
.map(obj => {
return obj.id;
})
.join(',');
},
placeholderText() {
if (this.selectedTokens.length === 0) {
return this.placeholder;
}
return '';
},
},
methods: {
handleTextInput(query) {
this.hideDropdownWithNoItems = false;
this.query = query;
this.loading = true;
this.retrieveUsers(query);
},
retrieveUsers: debounce(function debouncedRetrieveUsers() {
return Api.users(this.query, this.$options.queryOptions)
.then(response => {
this.users = response.data.map(token => ({
id: token.id,
name: token.name,
username: token.username,
avatar_url: token.avatar_url,
}));
this.loading = false;
})
.catch(() => {
this.loading = false;
});
}, USER_SEARCH_DELAY),
handleInput() {
this.$emit('input', this.newUsersToInvite);
},
handleBlur() {
this.hideDropdownWithNoItems = false;
},
handleFocus() {
// The modal auto-focuses on the input when opened.
// This prevents the dropdown from opening when the modal opens.
if (this.hasBeenFocused) {
this.loading = true;
this.retrieveUsers();
}
this.hasBeenFocused = true;
},
},
queryOptions: { exclude_internal: true, active: true },
};
</script>
<template>
<gl-token-selector
v-model="selectedTokens"
:dropdown-items="users"
:loading="loading"
:allow-user-defined-tokens="false"
:hide-dropdown-with-no-items="hideDropdownWithNoItems"
:placeholder="placeholderText"
:aria-labelledby="ariaLabelledby"
@blur="handleBlur"
@text-input="handleTextInput"
@input="handleInput"
@focus="handleFocus"
>
<template #token-content="{ token }">
<gl-avatar v-if="token.avatar_url" :src="token.avatar_url" :size="16" />
{{ token.name }}
</template>
<template #dropdown-item-content="{ dropdownItem }">
<gl-avatar-labeled
:src="dropdownItem.avatar_url"
:size="32"
:label="dropdownItem.name"
:sub-label="dropdownItem.username"
/>
</template>
</gl-token-selector>
</template>
export const USER_SEARCH_DELAY = 200;
...@@ -115,14 +115,10 @@ code { ...@@ -115,14 +115,10 @@ code {
background-color: $gray-50; background-color: $gray-50;
border-radius: $border-radius-default; border-radius: $border-radius-default;
.code > & { .code > &,
background-color: inherit;
padding: unset;
}
.build-trace & { .build-trace & {
background-color: inherit; background-color: inherit;
padding: inherit; padding: unset;
} }
} }
......
---
title: Fix code lines being cut-off on failed job tab
merge_request: 46885
author:
type: fixed
...@@ -85,31 +85,29 @@ describe('IterationSelect', () => { ...@@ -85,31 +85,29 @@ describe('IterationSelect', () => {
}); });
describe('when a user can edit', () => { describe('when a user can edit', () => {
it('opens the dropdown on click of the edit button', () => { it('opens the dropdown on click of the edit button', async () => {
createComponent({ props: { canEdit: true } }); createComponent({ props: { canEdit: true } });
expect(wrapper.find(GlDropdown).isVisible()).toBe(false); expect(wrapper.find(GlDropdown).isVisible()).toBe(false);
toggleDropdown(); toggleDropdown();
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
expect(wrapper.find(GlDropdown).isVisible()).toBe(true); expect(wrapper.find(GlDropdown).isVisible()).toBe(true);
});
}); });
it('focuses on the input', () => { it('focuses on the input', async () => {
createComponent({ props: { canEdit: true } }); createComponent({ props: { canEdit: true } });
const spy = jest.spyOn(wrapper.vm.$refs.search, 'focusInput'); const spy = jest.spyOn(wrapper.vm.$refs.search, 'focusInput');
toggleDropdown(); toggleDropdown();
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
});
}); });
it('stops propagation of the click event to avoid opening milestone dropdown', () => { it('stops propagation of the click event to avoid opening milestone dropdown', async () => {
const spy = jest.fn(); const spy = jest.fn();
createComponent({ props: { canEdit: true } }); createComponent({ props: { canEdit: true } });
...@@ -117,9 +115,8 @@ describe('IterationSelect', () => { ...@@ -117,9 +115,8 @@ describe('IterationSelect', () => {
toggleDropdown(spy); toggleDropdown(spy);
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
});
}); });
describe('when user is editing', () => { describe('when user is editing', () => {
...@@ -214,10 +211,9 @@ describe('IterationSelect', () => { ...@@ -214,10 +211,9 @@ describe('IterationSelect', () => {
}); });
}); });
it('sets the value returned from the mutation to currentIteration', () => { it('sets the value returned from the mutation to currentIteration', async () => {
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
expect(wrapper.vm.currentIteration).toBe('123'); expect(wrapper.vm.currentIteration).toBe('123');
});
}); });
}); });
...@@ -247,10 +243,9 @@ describe('IterationSelect', () => { ...@@ -247,10 +243,9 @@ describe('IterationSelect', () => {
.vm.$emit('click'); .vm.$emit('click');
}); });
it('calls createFlash with $expectedMsg', () => { it('calls createFlash with $expectedMsg', async () => {
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
expect(createFlash).toHaveBeenCalledWith(expectedMsg); expect(createFlash).toHaveBeenCalledWith(expectedMsg);
});
}); });
}); });
}); });
...@@ -263,33 +258,31 @@ describe('IterationSelect', () => { ...@@ -263,33 +258,31 @@ describe('IterationSelect', () => {
createComponent({}); createComponent({});
}); });
it('sets the search term', () => { it('sets the search term', async () => {
wrapper.find(GlSearchBoxByType).vm.$emit('input', 'testing'); wrapper.find(GlSearchBoxByType).vm.$emit('input', 'testing');
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
expect(wrapper.vm.searchTerm).toBe('testing'); expect(wrapper.vm.searchTerm).toBe('testing');
});
}); });
}); });
describe('when the user off clicks', () => { describe('when the user off clicks', () => {
describe('when the dropdown is open', () => { describe('when the dropdown is open', () => {
beforeEach(() => { beforeEach(async () => {
createComponent({}); createComponent({});
toggleDropdown(); toggleDropdown();
return wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
}); });
it('closes the dropdown', () => { it('closes the dropdown', async () => {
expect(wrapper.find(GlDropdown).isVisible()).toBe(true); expect(wrapper.find(GlDropdown).isVisible()).toBe(true);
toggleDropdown(); toggleDropdown();
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
expect(wrapper.find(GlDropdown).isVisible()).toBe(false); expect(wrapper.find(GlDropdown).isVisible()).toBe(false);
});
}); });
}); });
}); });
......
...@@ -29,71 +29,67 @@ describe('SidebarItemEpicsSelect', () => { ...@@ -29,71 +29,67 @@ describe('SidebarItemEpicsSelect', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('methods', () => { describe('methods', () => {
describe('getInitialEpicLoading', () => { describe('getInitialEpicLoading', () => {
it('should return `false` when `initialEpic` prop is provided', () => { it('should return `false` when `initialEpic` prop is provided', async () => {
wrapper.setProps({ wrapper.setProps({
initialEpic: mockEpic1, initialEpic: mockEpic1,
}); });
return wrapper.vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect(wrapper.vm.getInitialEpicLoading()).toBe(false); expect(wrapper.vm.getInitialEpicLoading()).toBe(false);
});
}); });
it('should return value of `sidebarStore.isFetching.epic` when `initialEpic` prop is null and `isFetching` is available', () => { it('should return value of `sidebarStore.isFetching.epic` when `initialEpic` prop is null and `isFetching` is available', async () => {
wrapper.setProps({ wrapper.setProps({
sidebarStore: { isFetching: { epic: true } }, sidebarStore: { isFetching: { epic: true } },
}); });
return wrapper.vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect(wrapper.vm.getInitialEpicLoading()).toBe(true); expect(wrapper.vm.getInitialEpicLoading()).toBe(true);
});
}); });
it('should return `false` when both `initialEpic` and `sidebarStore.isFetching` are unavailable', () => { it('should return `false` when both `initialEpic` and `sidebarStore.isFetching` are unavailable', async () => {
wrapper.setProps({ wrapper.setProps({
initialEpic: null, initialEpic: null,
sidebarStore: { isFetching: null }, sidebarStore: { isFetching: null },
}); });
return wrapper.vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect(wrapper.vm.getInitialEpicLoading()).toBe(false); expect(wrapper.vm.getInitialEpicLoading()).toBe(false);
});
}); });
}); });
describe('getEpic', () => { describe('getEpic', () => {
it('should return value of `initialEpic` as it is when it is available', () => { it('should return value of `initialEpic` as it is when it is available', async () => {
wrapper.setProps({ wrapper.setProps({
initialEpic: mockEpic1, initialEpic: mockEpic1,
}); });
return wrapper.vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect(wrapper.vm.getEpic()).toBe(mockEpic1); expect(wrapper.vm.getEpic()).toBe(mockEpic1);
});
}); });
it('should return value of `sidebarStore.epic` as it is when it is available', () => { it('should return value of `sidebarStore.epic` as it is when it is available', () => {
expect(wrapper.vm.getEpic()).toBe(mockEpic1); expect(wrapper.vm.getEpic()).toBe(mockEpic1);
}); });
it('should return No Epic object as it is when both `initialEpic` & `sidebarStore.epic` are unavailable', () => { it('should return No Epic object as it is when both `initialEpic` & `sidebarStore.epic` are unavailable', async () => {
wrapper.setProps({ wrapper.setProps({
initialEpic: null, initialEpic: null,
sidebarStore: { epic: null }, sidebarStore: { epic: null },
}); });
return wrapper.vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect(wrapper.vm.getEpic()).toEqual( expect(wrapper.vm.getEpic()).toEqual(
expect.objectContaining({ expect.objectContaining({
id: 0, id: 0,
title: 'No Epic', title: 'No Epic',
}), }),
); );
});
}); });
}); });
}); });
......
import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import Vue from 'vue';
import Status from 'ee/sidebar/components/status/status.vue'; import Status from 'ee/sidebar/components/status/status.vue';
import { healthStatus, healthStatusTextMap } from 'ee/sidebar/constants'; import { healthStatus, healthStatusTextMap } from 'ee/sidebar/constants';
...@@ -46,6 +45,7 @@ describe('Status', () => { ...@@ -46,6 +45,7 @@ describe('Status', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
it('shows the text "Status"', () => { it('shows the text "Status"', () => {
...@@ -106,7 +106,7 @@ describe('Status', () => { ...@@ -106,7 +106,7 @@ describe('Status', () => {
}); });
describe('remove status dropdown item', () => { describe('remove status dropdown item', () => {
it('is displayed when there is a status', () => { it('is displayed when there is a status', async () => {
const props = { const props = {
isEditable: true, isEditable: true,
status: healthStatus.AT_RISK, status: healthStatus.AT_RISK,
...@@ -116,9 +116,8 @@ describe('Status', () => { ...@@ -116,9 +116,8 @@ describe('Status', () => {
wrapper.vm.isDropdownShowing = true; wrapper.vm.isDropdownShowing = true;
wrapper.vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect(getRemoveStatusItem(wrapper).exists()).toBe(true); expect(getRemoveStatusItem(wrapper).exists()).toBe(true);
});
}); });
it('emits an onDropdownClick event with argument null when clicked', () => { it('emits an onDropdownClick event with argument null when clicked', () => {
...@@ -201,12 +200,11 @@ describe('Status', () => { ...@@ -201,12 +200,11 @@ describe('Status', () => {
mountStatus(props); mountStatus(props);
}); });
it('shows the dropdown when the Edit button is clicked', () => { it('shows the dropdown when the Edit button is clicked', async () => {
getEditButton(wrapper).trigger('click'); getEditButton(wrapper).trigger('click');
return Vue.nextTick().then(() => { await wrapper.vm.$nextTick();
expect(getDropdownClasses(wrapper)).toContain('show'); expect(getDropdownClasses(wrapper)).toContain('show');
});
}); });
}); });
...@@ -231,22 +229,20 @@ describe('Status', () => { ...@@ -231,22 +229,20 @@ describe('Status', () => {
).toContain(message); ).toContain(message);
}); });
it('hides form when the `edit` button is clicked', () => { it('hides form when the `edit` button is clicked', async () => {
getEditButton(wrapper).trigger('click'); getEditButton(wrapper).trigger('click');
return Vue.nextTick().then(() => { await wrapper.vm.$nextTick();
expect(getDropdownClasses(wrapper)).toContain('gl-display-none'); expect(getDropdownClasses(wrapper)).toContain('gl-display-none');
});
}); });
it('hides form when a dropdown item is clicked', () => { it('hides form when a dropdown item is clicked', async () => {
const dropdownItem = wrapper.findAll(GlDropdownItem).at(1); const dropdownItem = wrapper.findAll(GlDropdownItem).at(1);
dropdownItem.vm.$emit('click'); dropdownItem.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
expect(getDropdownClasses(wrapper)).toContain('gl-display-none'); expect(getDropdownClasses(wrapper)).toContain('gl-display-none');
});
}); });
}); });
...@@ -285,15 +281,14 @@ describe('Status', () => { ...@@ -285,15 +281,14 @@ describe('Status', () => {
// Test that "onTrack", "needsAttention", and "atRisk" values are emitted when form is submitted // Test that "onTrack", "needsAttention", and "atRisk" values are emitted when form is submitted
it.each(getIterableArray(Object.values(healthStatus)))( it.each(getIterableArray(Object.values(healthStatus)))(
'emits onFormSubmit event with argument "%s" when user selects the option and submits form', 'emits onFormSubmit event with argument "%s" when user selects the option and submits form',
(status, index) => { async (status, index) => {
wrapper wrapper
.findAll(GlDropdownItem) .findAll(GlDropdownItem)
.at(index + 1) .at(index + 1)
.vm.$emit('click', { preventDefault: () => null }); .vm.$emit('click', { preventDefault: () => null });
return Vue.nextTick().then(() => { await wrapper.vm.$nextTick();
expect(wrapper.emitted().onDropdownClick[0]).toEqual([status]); expect(wrapper.emitted().onDropdownClick[0]).toEqual([status]);
});
}, },
); );
}); });
......
import Vue from 'vue'; import { shallowMount } from '@vue/test-utils';
import { escape } from 'lodash'; import { escape } from 'lodash';
import ancestorsTree from 'ee/sidebar/components/ancestors_tree/ancestors_tree.vue'; import { GlLoadingIcon } from '@gitlab/ui';
import mountComponent from 'helpers/vue_mount_component_helper'; import AncestorsTree from 'ee/sidebar/components/ancestors_tree/ancestors_tree.vue';
describe('AncestorsTreeContainer', () => { describe('AncestorsTreeContainer', () => {
let vm; let wrapper;
const ancestors = [ const ancestors = [
{ id: 1, url: '', title: 'A', state: 'open' }, { id: 1, url: '', title: 'A', state: 'open' },
{ id: 2, url: '', title: 'B', state: 'open' }, { id: 2, url: '', title: 'B', state: 'open' },
]; ];
beforeEach(() => { const defaultProps = {
const AncestorsTreeContainer = Vue.extend(ancestorsTree); ancestors,
vm = mountComponent(AncestorsTreeContainer, { ancestors, isFetching: false }); isFetching: false,
}); };
const createComponent = (props = {}) => {
wrapper = shallowMount(AncestorsTree, {
propsData: { ...defaultProps, ...props },
});
};
afterEach(() => { afterEach(() => {
vm.$destroy(); if (wrapper) {
wrapper.destroy();
wrapper = null;
}
}); });
const findTooltip = () => wrapper.find('.collapse-truncated-title');
const containsTimeline = () => wrapper.contains('.vertical-timeline');
const containsValue = () => wrapper.contains('.value');
it('renders all ancestors rows', () => { it('renders all ancestors rows', () => {
expect(vm.$el.querySelectorAll('.vertical-timeline-row')).toHaveLength(ancestors.length); createComponent();
expect(wrapper.findAll('.vertical-timeline-row')).toHaveLength(ancestors.length);
}); });
it('renders tooltip with the immediate parent', () => { it('renders tooltip with the immediate parent', () => {
expect(vm.$el.querySelector('.collapse-truncated-title').innerText.trim()).toBe( createComponent();
ancestors.slice(-1)[0].title,
); expect(findTooltip().text()).toBe(ancestors.slice(-1)[0].title);
}); });
it('does not render timeline when fetching', () => { it('does not render timeline when fetching', () => {
vm.$props.isFetching = true; createComponent({
isFetching: true,
return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.vertical-timeline')).toBeNull();
expect(vm.$el.querySelector('.value')).toBeNull();
}); });
expect(containsTimeline()).toBe(false);
expect(containsValue()).toBe(false);
}); });
it('render `None` when ancestors is an empty array', () => { it('render `None` when ancestors is an empty array', () => {
vm.$props.ancestors = []; createComponent({
ancestors: [],
return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.vertical-timeline')).toBeNull();
expect(vm.$el.querySelector('.value')).not.toBeNull();
}); });
expect(containsTimeline()).toBe(false);
expect(containsValue()).not.toBe(false);
}); });
it('render loading icon when isFetching is true', () => { it('render loading icon when isFetching is true', () => {
vm.$props.isFetching = true; createComponent({
isFetching: true,
return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
}); });
expect(wrapper.contains(GlLoadingIcon)).toBe(true);
}); });
it('escapes html in the tooltip', () => { it('escapes html in the tooltip', () => {
const title = '<script>alert(1);</script>'; const title = '<script>alert(1);</script>';
const escapedTitle = escape(title); const escapedTitle = escape(title);
vm.$props.ancestors = [{ id: 1, url: '', title, state: 'open' }]; createComponent({
ancestors: [{ id: 1, url: '', title, state: 'open' }],
return vm.$nextTick().then(() => {
const tooltip = vm.$el.querySelector('.collapse-truncated-title');
expect(tooltip.innerText).toBe(escapedTitle);
}); });
expect(findTooltip().text()).toBe(escapedTitle);
}); });
}); });
import Vue from 'vue'; import { shallowMount } from '@vue/test-utils';
import sidebarWeight from 'ee/sidebar/components/weight/sidebar_weight.vue'; import SidebarWeight from 'ee/sidebar/components/weight/sidebar_weight.vue';
import SidebarMediator from 'ee/sidebar/sidebar_mediator'; import SidebarMediator from 'ee/sidebar/sidebar_mediator';
import SidebarStore from 'ee/sidebar/stores/sidebar_store'; import SidebarStore from 'ee/sidebar/stores/sidebar_store';
import mountComponent from 'helpers/vue_mount_component_helper';
import SidebarService from '~/sidebar/services/sidebar_service'; import SidebarService from '~/sidebar/services/sidebar_service';
import eventHub from '~/sidebar/event_hub'; import eventHub from '~/sidebar/event_hub';
import Mock from './ee_mock_data'; import Mock from './ee_mock_data';
describe('Sidebar Weight', () => { describe('Sidebar Weight', () => {
let vm;
let sidebarMediator; let sidebarMediator;
let SidebarWeight; let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(SidebarWeight, {
propsData: { ...props },
});
};
beforeEach(() => { beforeEach(() => {
SidebarWeight = Vue.extend(sidebarWeight);
// Set up the stores, services, etc // Set up the stores, services, etc
sidebarMediator = new SidebarMediator(Mock.mediator); sidebarMediator = new SidebarMediator(Mock.mediator);
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); if (wrapper) {
wrapper.destroy();
wrapper = null;
}
SidebarService.singleton = null; SidebarService.singleton = null;
SidebarStore.singleton = null; SidebarStore.singleton = null;
SidebarMediator.singleton = null; SidebarMediator.singleton = null;
...@@ -27,7 +33,8 @@ describe('Sidebar Weight', () => { ...@@ -27,7 +33,8 @@ describe('Sidebar Weight', () => {
it('calls the mediator updateWeight on event', () => { it('calls the mediator updateWeight on event', () => {
jest.spyOn(SidebarMediator.prototype, 'updateWeight').mockReturnValue(Promise.resolve()); jest.spyOn(SidebarMediator.prototype, 'updateWeight').mockReturnValue(Promise.resolve());
vm = mountComponent(SidebarWeight, {
createComponent({
mediator: sidebarMediator, mediator: sidebarMediator,
}); });
......
import Vue from 'vue'; import { shallowMount } from '@vue/test-utils';
import weight from 'ee/sidebar/components/weight/weight.vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper'; import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
import Weight from 'ee/sidebar/components/weight/weight.vue';
import eventHub from '~/sidebar/event_hub'; import eventHub from '~/sidebar/event_hub';
import { ENTER_KEY_CODE } from '~/lib/utils/keycodes'; import { ENTER_KEY_CODE } from '~/lib/utils/keycodes';
const DEFAULT_PROPS = {
weightNoneValue: 'None',
};
describe('Weight', () => { describe('Weight', () => {
let vm; let wrapper;
let Weight;
beforeEach(() => { const defaultProps = {
Weight = Vue.extend(weight); weightNoneValue: 'None',
}); };
const createComponent = (props = {}) => {
wrapper = shallowMount(Weight, {
propsData: { ...defaultProps, ...props },
});
};
afterEach(() => { afterEach(() => {
vm.$destroy(); if (wrapper) {
wrapper.destroy();
wrapper = null;
}
}); });
const containsCollapsedLoadingIcon = () => wrapper.contains('.js-weight-collapsed-loading-icon');
const containsLoadingIcon = () => wrapper.contains('.js-weight-loading-icon');
const findCollapsedLabel = () => wrapper.find('.js-weight-collapsed-weight-label');
const findLabelValue = () => wrapper.find('.js-weight-weight-label-value');
const findLabelNoValue = () => wrapper.find('.js-weight-weight-label .no-value');
const findCollapsedBlock = () => wrapper.find('.js-weight-collapsed-block');
const findEditLink = () => wrapper.find('.js-weight-edit-link');
const findRemoveLink = () => wrapper.find('.js-weight-remove-link');
const containsEditableField = () => wrapper.contains({ ref: 'editableField' });
const containsInputError = () => wrapper.contains('.gl-field-error');
it('shows loading spinner when fetching', () => { it('shows loading spinner when fetching', () => {
vm = mountComponent(Weight, { createComponent({
...DEFAULT_PROPS,
fetching: true, fetching: true,
}); });
expect(vm.$el.querySelector('.js-weight-collapsed-loading-icon')).not.toBeNull(); expect(containsCollapsedLoadingIcon()).toBe(true);
expect(vm.$el.querySelector('.js-weight-loading-icon')).not.toBeNull(); expect(containsLoadingIcon()).toBe(true);
}); });
it('shows loading spinner when loading', () => { it('shows loading spinner when loading', () => {
vm = mountComponent(Weight, { createComponent({
...DEFAULT_PROPS,
fetching: false, fetching: false,
loading: true, loading: true,
}); });
// We show the value in the collapsed view instead of the loading icon // We show the value in the collapsed view instead of the loading icon
expect(vm.$el.querySelector('.js-weight-collapsed-loading-icon')).toBeNull(); expect(containsCollapsedLoadingIcon()).toBe(false);
expect(vm.$el.querySelector('.js-weight-loading-icon')).not.toBeNull(); expect(containsLoadingIcon()).toBe(true);
}); });
it('shows weight value', () => { it('shows weight value', () => {
const WEIGHT = 3; const expectedWeight = 3;
vm = mountComponent(Weight, {
...DEFAULT_PROPS, createComponent({
fetching: false, fetching: false,
weight: WEIGHT, weight: expectedWeight,
}); });
expect(vm.$el.querySelector('.js-weight-collapsed-weight-label').textContent.trim()).toBe( expect(findCollapsedLabel().text()).toBe(`${expectedWeight}`);
`${WEIGHT}`, expect(findLabelValue().text()).toBe(`${expectedWeight}`);
);
expect(vm.$el.querySelector('.js-weight-weight-label-value').textContent.trim()).toBe(
`${WEIGHT}`,
);
}); });
it('shows weight no-value', () => { it('shows weight no-value', () => {
const WEIGHT = null; const expectedWeight = null;
vm = mountComponent(Weight, {
...DEFAULT_PROPS, createComponent({
fetching: false, fetching: false,
weight: WEIGHT, weight: expectedWeight,
}); });
expect(vm.$el.querySelector('.js-weight-collapsed-weight-label').textContent.trim()).toBe( expect(findCollapsedLabel().text()).toBe(defaultProps.weightNoneValue);
'None', expect(findLabelNoValue().text()).toBe(defaultProps.weightNoneValue);
);
expect(vm.$el.querySelector('.js-weight-weight-label .no-value').textContent.trim()).toBe(
'None',
);
}); });
it('adds `collapse-after-update` class when clicking the collapsed block', () => { it('adds `collapse-after-update` class when clicking the collapsed block', async () => {
vm = mountComponent(Weight, { createComponent();
...DEFAULT_PROPS,
});
vm.$el.querySelector('.js-weight-collapsed-block').click(); findCollapsedBlock().trigger('click');
return vm.$nextTick().then(() => { await wrapper.vm.$nextTick;
expect(vm.$el.classList.contains('collapse-after-update')).toBe(true);
}); expect(wrapper.classes()).toContain('collapse-after-update');
}); });
it('shows dropdown on "Edit" link click', () => { it('shows dropdown on "Edit" link click', async () => {
vm = mountComponent(Weight, { createComponent({
...DEFAULT_PROPS,
editable: true, editable: true,
}); });
expect(vm.shouldShowEditField).toBe(false); expect(containsEditableField()).toBe(false);
vm.$el.querySelector('.js-weight-edit-link').click(); findEditLink().trigger('click');
return vm.$nextTick().then(() => { await wrapper.vm.$nextTick;
expect(vm.shouldShowEditField).toBe(true);
}); expect(containsEditableField()).toBe(true);
}); });
it('emits event on input submission', () => { it('emits event on input submission', async () => {
const ID = 123; const mockId = 123;
const expectedWeightValue = '3'; const expectedWeightValue = '3';
jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
vm = mountComponent(Weight, {
...DEFAULT_PROPS, createComponent({
editable: true, editable: true,
id: ID, id: mockId,
}); });
vm.$el.querySelector('.js-weight-edit-link').click(); findEditLink().trigger('click');
return vm.$nextTick(() => { await wrapper.vm.$nextTick;
const event = new CustomEvent('keydown');
event.keyCode = ENTER_KEY_CODE;
vm.$refs.editableField.click(); const event = new CustomEvent('keydown');
vm.$refs.editableField.value = expectedWeightValue; event.keyCode = ENTER_KEY_CODE;
vm.$refs.editableField.dispatchEvent(event);
expect(vm.hasValidInput).toBe(true); const { editableField } = wrapper.vm.$refs;
expect(eventHub.$emit).toHaveBeenCalledWith('updateWeight', expectedWeightValue, ID); editableField.click();
}); editableField.value = expectedWeightValue;
editableField.dispatchEvent(event);
await wrapper.vm.$nextTick;
expect(containsInputError()).toBe(false);
expect(eventHub.$emit).toHaveBeenCalledWith('updateWeight', expectedWeightValue, mockId);
}); });
it('emits event on remove weight link click', () => { it('emits event on remove weight link click', async () => {
const ID = 123; const mockId = 234;
jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
vm = mountComponent(Weight, {
...DEFAULT_PROPS, createComponent({
editable: true, editable: true,
weight: 3, weight: 3,
id: ID, id: mockId,
}); });
vm.$el.querySelector('.js-weight-remove-link').click(); findRemoveLink().trigger('click');
return vm.$nextTick(() => { await wrapper.vm.$nextTick;
expect(vm.hasValidInput).toBe(true);
expect(eventHub.$emit).toHaveBeenCalledWith('updateWeight', '', ID); expect(containsInputError()).toBe(false);
}); expect(eventHub.$emit).toHaveBeenCalledWith('updateWeight', '', mockId);
}); });
it('triggers error on invalid negative integer value', () => { it('triggers error on invalid negative integer value', async () => {
vm = mountComponent(Weight, { createComponent({
...DEFAULT_PROPS,
editable: true, editable: true,
}); });
vm.$el.querySelector('.js-weight-edit-link').click(); findEditLink().trigger('click');
return vm.$nextTick(() => { await wrapper.vm.$nextTick;
const event = new CustomEvent('keydown');
event.keyCode = ENTER_KEY_CODE;
vm.$refs.editableField.click(); const event = new CustomEvent('keydown');
vm.$refs.editableField.value = -9001; event.keyCode = ENTER_KEY_CODE;
vm.$refs.editableField.dispatchEvent(event);
expect(vm.hasValidInput).toBe(false); const { editableField } = wrapper.vm.$refs;
}); editableField.click();
editableField.value = -9001;
editableField.dispatchEvent(event);
await wrapper.vm.$nextTick;
expect(containsInputError()).toBe(true);
}); });
describe('tracking', () => { describe('tracking', () => {
let trackingSpy; let trackingSpy;
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Weight, { createComponent({
...DEFAULT_PROPS,
editable: true, editable: true,
}); });
trackingSpy = mockTracking('_category_', vm.$el, (obj, what) =>
trackingSpy = mockTracking('_category_', wrapper.element, (obj, what) =>
jest.spyOn(obj, what).mockImplementation(() => {}), jest.spyOn(obj, what).mockImplementation(() => {}),
); );
}); });
...@@ -184,12 +190,12 @@ describe('Weight', () => { ...@@ -184,12 +190,12 @@ describe('Weight', () => {
unmockTracking(); unmockTracking();
}); });
it('calls trackEvent when "Edit" is clicked', () => { it('calls trackEvent when "Edit" is clicked', async () => {
triggerEvent(vm.$el.querySelector('.js-weight-edit-link')); triggerEvent(findEditLink().element);
return vm.$nextTick().then(() => { await wrapper.vm.$nextTick;
expect(trackingSpy).toHaveBeenCalled();
}); expect(trackingSpy).toHaveBeenCalled();
}); });
}); });
}); });
...@@ -14732,6 +14732,9 @@ msgstr "" ...@@ -14732,6 +14732,9 @@ msgstr ""
msgid "InviteMembersModal|Choose a role permission" msgid "InviteMembersModal|Choose a role permission"
msgstr "" msgstr ""
msgid "InviteMembersModal|Close invite team members"
msgstr ""
msgid "InviteMembersModal|GitLab member or Email address" msgid "InviteMembersModal|GitLab member or Email address"
msgstr "" msgstr ""
...@@ -14741,13 +14744,13 @@ msgstr "" ...@@ -14741,13 +14744,13 @@ msgstr ""
msgid "InviteMembersModal|Invite team members" msgid "InviteMembersModal|Invite team members"
msgstr "" msgstr ""
msgid "InviteMembersModal|Search for members to invite" msgid "InviteMembersModal|Members were successfully added"
msgstr "" msgstr ""
msgid "InviteMembersModal|User not invited. Feature coming soon!" msgid "InviteMembersModal|Search for members to invite"
msgstr "" msgstr ""
msgid "InviteMembersModal|Users were succesfully added" msgid "InviteMembersModal|Some of the members could not be added"
msgstr "" msgstr ""
msgid "InviteMembersModal|You're inviting members to the %{group_name} group" msgid "InviteMembersModal|You're inviting members to the %{group_name} group"
......
...@@ -2,11 +2,8 @@ ...@@ -2,11 +2,8 @@
module QA module QA
RSpec.describe 'Verify' do RSpec.describe 'Verify' do
describe 'Run pipeline', :requires_admin, :skip_live_env do describe 'Run pipeline', only: { subdomain: :staging } do
# [TODO]: Developer to remove :requires_admin and :skip_live_env once FF is removed in https://gitlab.com/gitlab-org/gitlab/-/issues/229632
context 'with web only rule' do context 'with web only rule' do
let(:feature_flag) { :new_pipeline_form }
let(:job_name) { 'test_job' } let(:job_name) { 'test_job' }
let(:project) do let(:project) do
Resource::Project.fabricate_via_api! do |project| Resource::Project.fabricate_via_api! do |project|
...@@ -20,33 +17,29 @@ module QA ...@@ -20,33 +17,29 @@ module QA
commit.commit_message = 'Add .gitlab-ci.yml' commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files( commit.add_files(
[ [
{ {
file_path: '.gitlab-ci.yml', file_path: '.gitlab-ci.yml',
content: <<~YAML content: <<~YAML
#{job_name}: #{job_name}:
tags: tags:
- #{project.name} - #{project.name}
script: echo 'OK' script: echo 'OK'
only: only:
- web - web
YAML
} YAML
}
] ]
) )
end end
end end
before do before do
Runtime::Feature.enable(feature_flag) # [TODO]: Developer to remove when feature flag is removed
Flow::Login.sign_in Flow::Login.sign_in
project.visit! project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines) Page::Project::Menu.perform(&:click_ci_cd_pipelines)
end end
after do
Runtime::Feature.disable(feature_flag) # [TODO]: Developer to remove when feature flag is removed
end
it 'can trigger pipeline', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/946' do it 'can trigger pipeline', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/946' do
Page::Project::Pipeline::Index.perform do |index| Page::Project::Pipeline::Index.perform do |index|
expect(index).not_to have_pipeline # should not auto trigger pipeline expect(index).not_to have_pipeline # should not auto trigger pipeline
......
...@@ -9,7 +9,7 @@ const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, O ...@@ -9,7 +9,7 @@ const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, O
const defaultAccessLevel = '10'; const defaultAccessLevel = '10';
const helpLink = 'https://example.com'; const helpLink = 'https://example.com';
const createComponent = () => { const createComponent = (data = {}) => {
return shallowMount(InviteMembersModal, { return shallowMount(InviteMembersModal, {
propsData: { propsData: {
groupId, groupId,
...@@ -18,9 +18,14 @@ const createComponent = () => { ...@@ -18,9 +18,14 @@ const createComponent = () => {
defaultAccessLevel, defaultAccessLevel,
helpLink, helpLink,
}, },
data() {
return data;
},
stubs: { stubs: {
GlSprintf,
'gl-modal': '<div><slot name="modal-footer"></slot><slot></slot></div>', 'gl-modal': '<div><slot name="modal-footer"></slot><slot></slot></div>',
'gl-dropdown': true,
'gl-dropdown-item': true,
GlSprintf,
}, },
}); });
}; };
...@@ -34,7 +39,7 @@ describe('InviteMembersModal', () => { ...@@ -34,7 +39,7 @@ describe('InviteMembersModal', () => {
}); });
const findDropdown = () => wrapper.find(GlDropdown); const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownItems = () => wrapper.findAll(GlDropdownItem); const findDropdownItems = () => findDropdown().findAll(GlDropdownItem);
const findDatepicker = () => wrapper.find(GlDatepicker); const findDatepicker = () => wrapper.find(GlDatepicker);
const findLink = () => wrapper.find(GlLink); const findLink = () => wrapper.find(GlLink);
const findCancelButton = () => wrapper.find({ ref: 'cancelButton' }); const findCancelButton = () => wrapper.find({ ref: 'cancelButton' });
...@@ -88,25 +93,69 @@ describe('InviteMembersModal', () => { ...@@ -88,25 +93,69 @@ describe('InviteMembersModal', () => {
format: 'json', format: 'json',
}; };
beforeEach(() => { describe('when the invite was sent successfully', () => {
wrapper = createComponent(); beforeEach(() => {
wrapper = createComponent();
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMember').mockResolvedValue({ data: postData });
jest.spyOn(Api, 'inviteGroupMember').mockResolvedValue({ data: postData }); wrapper.vm.submitForm(postData);
wrapper.vm.$toast = { show: jest.fn() }; });
wrapper.vm.submitForm(postData); it('displays the successful toastMessage', () => {
const toastMessageSuccessful = 'Members were successfully added';
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
toastMessageSuccessful,
wrapper.vm.toastOptions,
);
});
it('calls Api inviteGroupMember with the correct params', () => {
expect(Api.inviteGroupMember).toHaveBeenCalledWith(groupId, postData);
});
}); });
it('calls Api inviteGroupMember with the correct params', () => { describe('when sending the invite for a single member returned an api error', () => {
expect(Api.inviteGroupMember).toHaveBeenCalledWith(groupId, postData); const apiErrorMessage = 'Members already exists';
beforeEach(() => {
wrapper = createComponent({ newUsersToInvite: '123' });
wrapper.vm.$toast = { show: jest.fn() };
jest
.spyOn(Api, 'inviteGroupMember')
.mockRejectedValue({ response: { data: { message: apiErrorMessage } } });
findInviteButton().vm.$emit('click');
});
it('displays the api error message for the toastMessage', () => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
apiErrorMessage,
wrapper.vm.toastOptions,
);
});
}); });
describe('when the invite was sent successfully', () => { describe('when sending the invite for multiple members returned any error', () => {
const toastMessageSuccessful = 'Users were succesfully added'; const genericErrorMessage = 'Some of the members could not be added';
it('displays the successful toastMessage', () => { beforeEach(() => {
wrapper = createComponent({ newUsersToInvite: '123' });
wrapper.vm.$toast = { show: jest.fn() };
jest
.spyOn(Api, 'inviteGroupMember')
.mockRejectedValue({ response: { data: { success: false } } });
findInviteButton().vm.$emit('click');
});
it('displays the expected toastMessage', () => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith( expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
toastMessageSuccessful, genericErrorMessage,
wrapper.vm.toastOptions, wrapper.vm.toastOptions,
); );
}); });
......
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { GlTokenSelector } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
const label = 'testgroup';
const placeholder = 'Search for a member';
const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' };
const user2 = { id: 2, name: 'Name Two', username: 'two_2', avatar_url: '' };
const allUsers = [user1, user2];
const createComponent = () => {
return shallowMount(MembersTokenSelect, {
propsData: {
ariaLabelledby: label,
placeholder,
},
});
};
describe('MembersTokenSelect', () => {
let wrapper;
beforeEach(() => {
jest.spyOn(Api, 'users').mockResolvedValue({ data: allUsers });
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findTokenSelector = () => wrapper.find(GlTokenSelector);
describe('rendering the token-selector component', () => {
it('renders with the correct props', () => {
const expectedProps = {
ariaLabelledby: label,
placeholder,
};
expect(findTokenSelector().props()).toEqual(expect.objectContaining(expectedProps));
});
});
describe('users', () => {
describe('when input is focused for the first time (modal auto-focus)', () => {
it('does not call the API', async () => {
findTokenSelector().vm.$emit('focus');
await waitForPromises();
expect(Api.users).not.toHaveBeenCalled();
});
});
describe('when input is manually focused', () => {
it('calls the API and sets dropdown items as request result', async () => {
const tokenSelector = findTokenSelector();
tokenSelector.vm.$emit('focus');
tokenSelector.vm.$emit('blur');
tokenSelector.vm.$emit('focus');
await waitForPromises();
expect(tokenSelector.props('dropdownItems')).toMatchObject(allUsers);
expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false);
});
});
describe('when text input is typed in', () => {
it('calls the API with search parameter', async () => {
const searchParam = 'One';
const tokenSelector = findTokenSelector();
tokenSelector.vm.$emit('text-input', searchParam);
await waitForPromises();
expect(Api.users).toHaveBeenCalledWith(searchParam, wrapper.vm.$options.queryOptions);
expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false);
});
});
describe('when user is selected', () => {
it('emits `input` event with selected users', () => {
findTokenSelector().vm.$emit('input', [
{ id: 1, name: 'John Smith' },
{ id: 2, name: 'Jane Doe' },
]);
expect(wrapper.emitted().input[0][0]).toBe('1,2');
});
});
});
describe('when text input is blurred', () => {
it('clears text input', async () => {
const tokenSelector = findTokenSelector();
tokenSelector.vm.$emit('blur');
await nextTick();
expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment