Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js')
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js558
1 files changed, 558 insertions, 0 deletions
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
new file mode 100644
index 00000000000..824f6d49c65
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
@@ -0,0 +1,558 @@
+import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { cloneDeep } from 'lodash';
+import { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
+import { IssuableType } from '~/issue_show/constants';
+import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
+import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
+import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
+import SidebarInviteMembers from '~/sidebar/components/assignees/sidebar_invite_members.vue';
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+import { ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants';
+import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
+import getIssueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
+import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
+import {
+ issuableQueryResponse,
+ searchQueryResponse,
+ updateIssueAssigneesMutationResponse,
+} from '../../mock_data';
+
+jest.mock('~/flash');
+
+const updateIssueAssigneesMutationSuccess = jest
+ .fn()
+ .mockResolvedValue(updateIssueAssigneesMutationResponse);
+const mockError = jest.fn().mockRejectedValue('Error!');
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+const initialAssignees = [
+ {
+ id: 'some-user',
+ avatarUrl: 'some-user-avatar',
+ name: 'test',
+ username: 'test',
+ webUrl: '/test',
+ },
+];
+
+describe('Sidebar assignees widget', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const findAssignees = () => wrapper.findComponent(IssuableAssignees);
+ const findRealtimeAssignees = () => wrapper.findComponent(SidebarAssigneesRealtime);
+ const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
+ const findDropdown = () => wrapper.findComponent(MultiSelectDropdown);
+ const findInviteMembersLink = () => wrapper.findComponent(SidebarInviteMembers);
+ const findSearchField = () => wrapper.findComponent(GlSearchBoxByType);
+
+ const findParticipantsLoading = () => wrapper.find('[data-testid="loading-participants"]');
+ const findSelectedParticipants = () => wrapper.findAll('[data-testid="selected-participant"]');
+ const findUnselectedParticipants = () =>
+ wrapper.findAll('[data-testid="unselected-participant"]');
+ const findCurrentUser = () => wrapper.findAll('[data-testid="current-user"]');
+ const findUnassignLink = () => wrapper.find('[data-testid="unassign"]');
+ const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]');
+
+ const expandDropdown = () => wrapper.vm.$refs.toggle.expand();
+
+ const createComponent = ({
+ search = '',
+ issuableQueryHandler = jest.fn().mockResolvedValue(issuableQueryResponse),
+ searchQueryHandler = jest.fn().mockResolvedValue(searchQueryResponse),
+ updateIssueAssigneesMutationHandler = updateIssueAssigneesMutationSuccess,
+ props = {},
+ provide = {},
+ } = {}) => {
+ fakeApollo = createMockApollo([
+ [getIssueParticipantsQuery, issuableQueryHandler],
+ [searchUsersQuery, searchQueryHandler],
+ [updateIssueAssigneesMutation, updateIssueAssigneesMutationHandler],
+ ]);
+ wrapper = shallowMount(SidebarAssigneesWidget, {
+ localVue,
+ apolloProvider: fakeApollo,
+ propsData: {
+ iid: '1',
+ fullPath: '/mygroup/myProject',
+ ...props,
+ },
+ data() {
+ return {
+ search,
+ selected: [],
+ };
+ },
+ provide: {
+ canUpdate: true,
+ rootPath: '/',
+ ...provide,
+ },
+ stubs: {
+ SidebarEditableItem,
+ MultiSelectDropdown,
+ GlSearchBoxByType,
+ GlDropdown,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ gon.current_username = 'root';
+ gon.current_user_fullname = 'Administrator';
+ gon.current_user_avatar_url = '/root';
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ fakeApollo = null;
+ delete gon.current_username;
+ });
+
+ describe('with passed initial assignees', () => {
+ it('passes `initialLoading` as false to editable item', () => {
+ createComponent({
+ props: {
+ initialAssignees,
+ },
+ });
+
+ expect(findEditableItem().props('initialLoading')).toBe(false);
+ });
+
+ it('renders an initial assignees list with initialAssignees prop', () => {
+ createComponent({
+ props: {
+ initialAssignees,
+ },
+ });
+
+ expect(findAssignees().props('users')).toEqual(initialAssignees);
+ });
+
+ it('renders a collapsible item title calculated with initial assignees length', () => {
+ createComponent({
+ props: {
+ initialAssignees,
+ },
+ });
+
+ expect(findEditableItem().props('title')).toBe('Assignee');
+ });
+
+ describe('when expanded', () => {
+ it('renders a loading spinner if participants are loading', () => {
+ createComponent({
+ props: {
+ initialAssignees,
+ },
+ });
+ expandDropdown();
+
+ expect(findParticipantsLoading().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('without passed initial assignees', () => {
+ it('passes `initialLoading` as true to editable item', () => {
+ createComponent();
+
+ expect(findEditableItem().props('initialLoading')).toBe(true);
+ });
+
+ it('renders assignees list from API response when resolved', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findAssignees().props('users')).toEqual(
+ issuableQueryResponse.data.workspace.issuable.assignees.nodes,
+ );
+ });
+
+ it('renders an error when issuable query is rejected', async () => {
+ createComponent({
+ issuableQueryHandler: mockError,
+ });
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'An error occurred while fetching participants.',
+ });
+ });
+
+ it('assigns current user when clicking `Assign self`', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ findAssignees().vm.$emit('assign-self');
+
+ expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({
+ assigneeUsernames: 'root',
+ fullPath: '/mygroup/myProject',
+ iid: '1',
+ });
+
+ await waitForPromises();
+
+ expect(
+ findAssignees()
+ .props('users')
+ .some((user) => user.username === 'root'),
+ ).toBe(true);
+ });
+
+ it('emits an event with assignees list on successful mutation', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ findAssignees().vm.$emit('assign-self');
+
+ expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({
+ assigneeUsernames: 'root',
+ fullPath: '/mygroup/myProject',
+ iid: '1',
+ });
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('assignees-updated')).toEqual([
+ [
+ [
+ {
+ __typename: 'User',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: '/root',
+ status: null,
+ },
+ ],
+ ],
+ ]);
+ });
+
+ it('renders current user if they are not in participants or assignees', async () => {
+ gon.current_username = 'random';
+ gon.current_user_fullname = 'Mr Random';
+ gon.current_user_avatar_url = '/random';
+
+ createComponent();
+ await waitForPromises();
+ expandDropdown();
+
+ expect(findCurrentUser().exists()).toBe(true);
+ });
+
+ describe('when expanded', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ expandDropdown();
+ });
+
+ it('collapses the widget on multiselect dropdown toggle event', async () => {
+ findDropdown().vm.$emit('toggle');
+ await nextTick();
+ expect(findDropdown().isVisible()).toBe(false);
+ });
+
+ it('renders participants list with correct amount of selected and unselected', async () => {
+ expect(findSelectedParticipants()).toHaveLength(1);
+ expect(findUnselectedParticipants()).toHaveLength(2);
+ });
+
+ it('does not render current user if they are in participants', () => {
+ expect(findCurrentUser().exists()).toBe(false);
+ });
+
+ it('unassigns all participants when clicking on `Unassign`', () => {
+ findUnassignLink().vm.$emit('click');
+ findEditableItem().vm.$emit('close');
+
+ expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({
+ assigneeUsernames: [],
+ fullPath: '/mygroup/myProject',
+ iid: '1',
+ });
+ });
+ });
+
+ describe('when multiselect is disabled', () => {
+ beforeEach(async () => {
+ createComponent({ props: { multipleAssignees: false } });
+ await waitForPromises();
+ expandDropdown();
+ });
+
+ it('adds a single assignee when clicking on unselected user', async () => {
+ findUnselectedParticipants().at(0).vm.$emit('click');
+
+ expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({
+ assigneeUsernames: ['root'],
+ fullPath: '/mygroup/myProject',
+ iid: '1',
+ });
+ });
+
+ it('removes an assignee when clicking on selected user', () => {
+ findSelectedParticipants().at(0).vm.$emit('click', new Event('click'));
+
+ expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({
+ assigneeUsernames: [],
+ fullPath: '/mygroup/myProject',
+ iid: '1',
+ });
+ });
+ });
+
+ describe('when multiselect is enabled', () => {
+ beforeEach(async () => {
+ createComponent({ props: { multipleAssignees: true } });
+ await waitForPromises();
+ expandDropdown();
+ });
+
+ it('adds a few assignees after clicking on unselected users and closing a dropdown', () => {
+ findUnselectedParticipants().at(0).vm.$emit('click');
+ findUnselectedParticipants().at(1).vm.$emit('click');
+ findEditableItem().vm.$emit('close');
+
+ expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({
+ assigneeUsernames: ['francina.skiles', 'root', 'johndoe'],
+ fullPath: '/mygroup/myProject',
+ iid: '1',
+ });
+ });
+
+ it('removes an assignee when clicking on selected user and then closing dropdown', () => {
+ findSelectedParticipants().at(0).vm.$emit('click', new Event('click'));
+
+ findEditableItem().vm.$emit('close');
+
+ expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({
+ assigneeUsernames: [],
+ fullPath: '/mygroup/myProject',
+ iid: '1',
+ });
+ });
+
+ it('does not call a mutation when clicking on participants until dropdown is closed', () => {
+ findUnselectedParticipants().at(0).vm.$emit('click');
+ findSelectedParticipants().at(0).vm.$emit('click', new Event('click'));
+
+ expect(updateIssueAssigneesMutationSuccess).not.toHaveBeenCalled();
+ });
+ });
+
+ it('shows an error if update assignees mutation is rejected', async () => {
+ createComponent({ updateIssueAssigneesMutationHandler: mockError });
+ await waitForPromises();
+ expandDropdown();
+
+ findUnassignLink().vm.$emit('click');
+ findEditableItem().vm.$emit('close');
+
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'An error occurred while updating assignees.',
+ });
+ });
+
+ describe('when searching', () => {
+ it('does not show loading spinner when debounce timer is still running', async () => {
+ createComponent({ search: 'roo' });
+ await waitForPromises();
+ expandDropdown();
+
+ expect(findParticipantsLoading().exists()).toBe(false);
+ });
+
+ it('shows loading spinner when searching for users', async () => {
+ createComponent({ search: 'roo' });
+ await waitForPromises();
+ expandDropdown();
+ jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY);
+ await nextTick();
+
+ expect(findParticipantsLoading().exists()).toBe(true);
+ });
+
+ it('renders a list of found users and external participants matching search term', async () => {
+ const responseCopy = cloneDeep(issuableQueryResponse);
+ responseCopy.data.workspace.issuable.participants.nodes.push({
+ id: 'gid://gitlab/User/5',
+ avatarUrl: '/someavatar',
+ name: 'Roodie',
+ username: 'roodie',
+ webUrl: '/roodie',
+ status: null,
+ });
+
+ const issuableQueryHandler = jest.fn().mockResolvedValue(responseCopy);
+
+ createComponent({ issuableQueryHandler });
+ await waitForPromises();
+ expandDropdown();
+
+ findSearchField().vm.$emit('input', 'roo');
+ await nextTick();
+
+ jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY);
+ await nextTick();
+ await waitForPromises();
+
+ expect(findUnselectedParticipants()).toHaveLength(3);
+ });
+
+ it('renders a list of found users only if no external participants match search term', async () => {
+ createComponent({ search: 'roo' });
+ await waitForPromises();
+ expandDropdown();
+ jest.advanceTimersByTime(250);
+ await nextTick();
+ await waitForPromises();
+
+ expect(findUnselectedParticipants()).toHaveLength(2);
+ });
+
+ it('shows a message about no matches if search returned an empty list', async () => {
+ const responseCopy = cloneDeep(searchQueryResponse);
+ responseCopy.data.workspace.users.nodes = [];
+
+ createComponent({
+ search: 'roo',
+ searchQueryHandler: jest.fn().mockResolvedValue(responseCopy),
+ });
+ await waitForPromises();
+ expandDropdown();
+ jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY);
+ await nextTick();
+ await waitForPromises();
+
+ expect(findUnselectedParticipants()).toHaveLength(0);
+ expect(findEmptySearchResults().exists()).toBe(true);
+ });
+
+ it('shows an error if search query was rejected', async () => {
+ createComponent({ search: 'roo', searchQueryHandler: mockError });
+ await waitForPromises();
+ expandDropdown();
+ jest.advanceTimersByTime(250);
+ await nextTick();
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'An error occurred while searching users.',
+ });
+ });
+ });
+ });
+
+ describe('when user is not signed in', () => {
+ beforeEach(() => {
+ gon.current_username = undefined;
+ createComponent();
+ });
+
+ it('does not show current user in the dropdown', () => {
+ expandDropdown();
+ expect(findCurrentUser().exists()).toBe(false);
+ });
+
+ it('passes signedIn prop as false to IssuableAssignees', () => {
+ expect(findAssignees().props('signedIn')).toBe(false);
+ });
+ });
+
+ it('when realtime feature flag is disabled', async () => {
+ createComponent();
+ await waitForPromises();
+ expect(findRealtimeAssignees().exists()).toBe(false);
+ });
+
+ it('when realtime feature flag is enabled', async () => {
+ createComponent({
+ provide: {
+ glFeatures: {
+ realTimeIssueSidebar: true,
+ },
+ },
+ });
+ await waitForPromises();
+ expect(findRealtimeAssignees().exists()).toBe(true);
+ });
+
+ describe('when making changes to participants list', () => {
+ beforeEach(async () => {
+ createComponent();
+ });
+
+ it('passes falsy `isDirty` prop to editable item if no changes to selected users were made', () => {
+ expandDropdown();
+ expect(findEditableItem().props('isDirty')).toBe(false);
+ });
+
+ it('passes truthy `isDirty` prop if selected users list was changed', async () => {
+ expandDropdown();
+ expect(findEditableItem().props('isDirty')).toBe(false);
+ findUnselectedParticipants().at(0).vm.$emit('click');
+ await nextTick();
+ expect(findEditableItem().props('isDirty')).toBe(true);
+ });
+
+ it('passes falsy `isDirty` prop after dropdown is closed', async () => {
+ expandDropdown();
+ findUnselectedParticipants().at(0).vm.$emit('click');
+ findEditableItem().vm.$emit('close');
+ await waitForPromises();
+ expect(findEditableItem().props('isDirty')).toBe(false);
+ });
+ });
+
+ it('does not render invite members link on non-issue sidebar', async () => {
+ createComponent({ props: { issuableType: IssuableType.MergeRequest } });
+ await waitForPromises();
+ expect(findInviteMembersLink().exists()).toBe(false);
+ });
+
+ it('does not render invite members link if `directlyInviteMembers` and `indirectlyInviteMembers` were not passed', async () => {
+ createComponent();
+ await waitForPromises();
+ expect(findInviteMembersLink().exists()).toBe(false);
+ });
+
+ it('renders invite members link if `directlyInviteMembers` is true', async () => {
+ createComponent({
+ provide: {
+ directlyInviteMembers: true,
+ },
+ });
+ await waitForPromises();
+ expect(findInviteMembersLink().exists()).toBe(true);
+ });
+
+ it('renders invite members link if `indirectlyInviteMembers` is true', async () => {
+ createComponent({
+ provide: {
+ indirectlyInviteMembers: true,
+ },
+ });
+ await waitForPromises();
+ expect(findInviteMembersLink().exists()).toBe(true);
+ });
+});