summaryrefslogtreecommitdiff
path: root/spec/frontend/sidebar
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-04-20 23:50:22 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-04-20 23:50:22 +0000
commit9dc93a4519d9d5d7be48ff274127136236a3adb3 (patch)
tree70467ae3692a0e35e5ea56bcb803eb512a10bedb /spec/frontend/sidebar
parent4b0f34b6d759d6299322b3a54453e930c6121ff0 (diff)
downloadgitlab-ce-9dc93a4519d9d5d7be48ff274127136236a3adb3.tar.gz
Add latest changes from gitlab-org/gitlab@13-11-stable-eev13.11.0-rc43
Diffstat (limited to 'spec/frontend/sidebar')
-rw-r--r--spec/frontend/sidebar/assignees_realtime_spec.js15
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js558
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js33
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js59
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js43
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js4
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js4
-rw-r--r--spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js19
-rw-r--r--spec/frontend/sidebar/components/due_date/sidebar_due_date_widget_spec.js106
-rw-r--r--spec/frontend/sidebar/components/reference/sidebar_reference_widget_spec.js70
-rw-r--r--spec/frontend/sidebar/issuable_assignees_spec.js19
-rw-r--r--spec/frontend/sidebar/mock_data.js156
12 files changed, 1019 insertions, 67 deletions
diff --git a/spec/frontend/sidebar/assignees_realtime_spec.js b/spec/frontend/sidebar/assignees_realtime_spec.js
index 0fab6a29f71..f0a6fa40d67 100644
--- a/spec/frontend/sidebar/assignees_realtime_spec.js
+++ b/spec/frontend/sidebar/assignees_realtime_spec.js
@@ -1,7 +1,7 @@
import ActionCable from '@rails/actioncable';
import { shallowMount } from '@vue/test-utils';
-import query from '~/issuable_sidebar/queries/issue_sidebar.query.graphql';
import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
+import { assigneesQueries } from '~/sidebar/constants';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import Mock from './mock_data';
@@ -18,18 +18,19 @@ describe('Assignees Realtime', () => {
let wrapper;
let mediator;
- const createComponent = () => {
+ const createComponent = (issuableType = 'issue') => {
wrapper = shallowMount(AssigneesRealtime, {
propsData: {
issuableIid: '1',
mediator,
projectPath: 'path/to/project',
+ issuableType,
},
mocks: {
$apollo: {
- query,
+ query: assigneesQueries[issuableType].query,
queries: {
- project: {
+ workspace: {
refetch: jest.fn(),
},
},
@@ -51,8 +52,8 @@ describe('Assignees Realtime', () => {
describe('when handleFetchResult is called from smart query', () => {
it('sets assignees to the store', () => {
const data = {
- project: {
- issue: {
+ workspace: {
+ issuable: {
assignees: {
nodes: [{ id: 'gid://gitlab/Environments/123', avatarUrl: 'url' }],
},
@@ -95,7 +96,7 @@ describe('Assignees Realtime', () => {
wrapper.vm.received({ event: 'updated' });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.$apollo.queries.project.refetch).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.$apollo.queries.workspace.refetch).toHaveBeenCalledTimes(1);
});
});
});
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);
+ });
+});
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
index 4ee12838491..84b192aaf41 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
@@ -5,7 +5,7 @@ import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'
describe('boards sidebar remove issue', () => {
let wrapper;
- const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findLoader = () => wrapper.findComponent(GlLoadingIcon);
const findEditButton = () => wrapper.find('[data-testid="edit-button"]');
const findTitle = () => wrapper.find('[data-testid="title"]');
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
@@ -117,4 +117,35 @@ describe('boards sidebar remove issue', () => {
expect(wrapper.emitted().close).toBeUndefined();
});
+
+ it('renders `Edit` test when passed `isDirty` prop is false', () => {
+ createComponent({ props: { isDirty: false }, canUpdate: true });
+
+ expect(findEditButton().text()).toBe('Edit');
+ });
+
+ it('renders `Apply` test when passed `isDirty` prop is true', () => {
+ createComponent({ props: { isDirty: true }, canUpdate: true });
+
+ expect(findEditButton().text()).toBe('Apply');
+ });
+
+ describe('when initial loading is true', () => {
+ beforeEach(() => {
+ createComponent({ props: { initialLoading: true } });
+ });
+
+ it('renders loading icon', () => {
+ expect(findLoader().exists()).toBe(true);
+ });
+
+ it('does not render edit button', () => {
+ expect(findEditButton().exists()).toBe(false);
+ });
+
+ it('does not render collapsed and expanded content', () => {
+ expect(findCollapsed().exists()).toBe(false);
+ expect(findExpanded().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
new file mode 100644
index 00000000000..06f7da3d1ab
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
@@ -0,0 +1,59 @@
+import { shallowMount } from '@vue/test-utils';
+import InviteMemberModal from '~/invite_member/components/invite_member_modal.vue';
+import InviteMemberTrigger from '~/invite_member/components/invite_member_trigger.vue';
+import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
+import SidebarInviteMembers from '~/sidebar/components/assignees/sidebar_invite_members.vue';
+
+const testProjectMembersPath = 'test-path';
+
+describe('Sidebar invite members component', () => {
+ let wrapper;
+
+ const findDirectInviteLink = () => wrapper.findComponent(InviteMembersTrigger);
+ const findIndirectInviteLink = () => wrapper.findComponent(InviteMemberTrigger);
+ const findInviteModal = () => wrapper.findComponent(InviteMemberModal);
+
+ const createComponent = ({ directlyInviteMembers = false } = {}) => {
+ wrapper = shallowMount(SidebarInviteMembers, {
+ provide: {
+ directlyInviteMembers,
+ projectMembersPath: testProjectMembersPath,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when directly inviting members', () => {
+ beforeEach(() => {
+ createComponent({ directlyInviteMembers: true });
+ });
+
+ it('renders a direct link to project members path', () => {
+ expect(findDirectInviteLink().exists()).toBe(true);
+ });
+
+ it('does not render invite members trigger and modal components', () => {
+ expect(findIndirectInviteLink().exists()).toBe(false);
+ expect(findInviteModal().exists()).toBe(false);
+ });
+ });
+
+ describe('when indirectly inviting members', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not render a direct link to project members path', () => {
+ expect(findDirectInviteLink().exists()).toBe(false);
+ });
+
+ it('does not render invite members trigger and modal components', () => {
+ expect(findIndirectInviteLink().exists()).toBe(true);
+ expect(findInviteModal().exists()).toBe(true);
+ expect(findInviteModal().props('membersPath')).toBe(testProjectMembersPath);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
new file mode 100644
index 00000000000..88a5f4ea8b7
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
@@ -0,0 +1,43 @@
+import { GlAvatarLabeled } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
+
+const user = {
+ name: 'John Doe',
+ username: 'johndoe',
+ webUrl: '/link',
+ avatarUrl: '/avatar',
+};
+
+describe('Sidebar participant component', () => {
+ let wrapper;
+
+ const findAvatar = () => wrapper.findComponent(GlAvatarLabeled);
+
+ const createComponent = (status = null) => {
+ wrapper = shallowMount(SidebarParticipant, {
+ propsData: {
+ user: {
+ ...user,
+ status,
+ },
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('when user is not busy', () => {
+ createComponent();
+
+ expect(findAvatar().props('label')).toBe(user.name);
+ });
+
+ it('when user is busy', () => {
+ createComponent({ availability: 'BUSY' });
+
+ expect(findAvatar().props('label')).toBe(`${user.name} (Busy)`);
+ });
+});
diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
index d5e6310ed38..28a19fb9df6 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
@@ -20,11 +20,9 @@ describe('Sidebar Confidentiality Form', () => {
mutate = jest.fn().mockResolvedValue('Success'),
} = {}) => {
wrapper = shallowMount(SidebarConfidentialityForm, {
- provide: {
+ propsData: {
fullPath: 'group/project',
iid: '1',
- },
- propsData: {
confidential: false,
issuableType: 'issue',
...props,
diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
index 20a5be9b518..707215d0739 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
@@ -35,11 +35,11 @@ describe('Sidebar Confidentiality Widget', () => {
localVue,
apolloProvider: fakeApollo,
provide: {
- fullPath: 'group/project',
- iid: '1',
canUpdate: true,
},
propsData: {
+ fullPath: 'group/project',
+ iid: '1',
issuableType: 'issue',
},
stubs: {
diff --git a/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js b/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js
index 704847f65bf..699b2bbd0b1 100644
--- a/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js
+++ b/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js
@@ -1,22 +1,17 @@
-import { getByText } from '@testing-library/dom';
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import CopyEmailToClipboard from '~/sidebar/components/copy_email_to_clipboard.vue';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue';
describe('CopyEmailToClipboard component', () => {
- const sampleEmail = 'sample+email@test.com';
+ const mockIssueEmailAddress = 'sample+email@test.com';
- const wrapper = mount(CopyEmailToClipboard, {
+ const wrapper = shallowMount(CopyEmailToClipboard, {
propsData: {
- copyText: sampleEmail,
+ issueEmailAddress: mockIssueEmailAddress,
},
});
- it('renders the Issue email text with the forwardable email', () => {
- expect(getByText(wrapper.element, `Issue email: ${sampleEmail}`)).not.toBeNull();
- });
-
- it('finds ClipboardButton with the correct props', () => {
- expect(wrapper.find(ClipboardButton).props('text')).toBe(sampleEmail);
+ it('sets CopyableField `value` prop to issueEmailAddress', () => {
+ expect(wrapper.find(CopyableField).props('value')).toBe(mockIssueEmailAddress);
});
});
diff --git a/spec/frontend/sidebar/components/due_date/sidebar_due_date_widget_spec.js b/spec/frontend/sidebar/components/due_date/sidebar_due_date_widget_spec.js
new file mode 100644
index 00000000000..f58ceb0f1be
--- /dev/null
+++ b/spec/frontend/sidebar/components/due_date/sidebar_due_date_widget_spec.js
@@ -0,0 +1,106 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue 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 SidebarDueDateWidget from '~/sidebar/components/due_date/sidebar_due_date_widget.vue';
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
+import { issueDueDateResponse } from '../../mock_data';
+
+jest.mock('~/flash');
+
+Vue.use(VueApollo);
+
+describe('Sidebar Due date Widget', () => {
+ let wrapper;
+ let fakeApollo;
+ const date = '2021-04-15';
+
+ const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
+ const findFormattedDueDate = () => wrapper.find("[data-testid='sidebar-duedate-value']");
+
+ const createComponent = ({
+ dueDateQueryHandler = jest.fn().mockResolvedValue(issueDueDateResponse()),
+ } = {}) => {
+ fakeApollo = createMockApollo([[issueDueDateQuery, dueDateQueryHandler]]);
+
+ wrapper = shallowMount(SidebarDueDateWidget, {
+ apolloProvider: fakeApollo,
+ provide: {
+ fullPath: 'group/project',
+ iid: '1',
+ canUpdate: true,
+ },
+ propsData: {
+ issuableType: 'issue',
+ },
+ stubs: {
+ SidebarEditableItem,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ it('passes a `loading` prop as true to editable item when query is loading', () => {
+ createComponent();
+
+ expect(findEditableItem().props('loading')).toBe(true);
+ });
+
+ describe('when issue has no due date', () => {
+ beforeEach(async () => {
+ createComponent({
+ dueDateQueryHandler: jest.fn().mockResolvedValue(issueDueDateResponse(null)),
+ });
+ await waitForPromises();
+ });
+
+ it('passes a `loading` prop as false to editable item', () => {
+ expect(findEditableItem().props('loading')).toBe(false);
+ });
+
+ it('dueDate is null by default', () => {
+ expect(findFormattedDueDate().text()).toBe('None');
+ });
+
+ it('emits `dueDateUpdated` event with a `null` payload', () => {
+ expect(wrapper.emitted('dueDateUpdated')).toEqual([[null]]);
+ });
+ });
+
+ describe('when issue has due date', () => {
+ beforeEach(async () => {
+ createComponent({
+ dueDateQueryHandler: jest.fn().mockResolvedValue(issueDueDateResponse(date)),
+ });
+ await waitForPromises();
+ });
+
+ it('passes a `loading` prop as false to editable item', () => {
+ expect(findEditableItem().props('loading')).toBe(false);
+ });
+
+ it('has dueDate', () => {
+ expect(findFormattedDueDate().text()).toBe('Apr 15, 2021');
+ });
+
+ it('emits `dueDateUpdated` event with the date payload', () => {
+ expect(wrapper.emitted('dueDateUpdated')).toEqual([[date]]);
+ });
+ });
+
+ it('displays a flash message when query is rejected', async () => {
+ createComponent({
+ dueDateQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
+ });
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/sidebar/components/reference/sidebar_reference_widget_spec.js b/spec/frontend/sidebar/components/reference/sidebar_reference_widget_spec.js
index 1dbb7702a15..cc428693930 100644
--- a/spec/frontend/sidebar/components/reference/sidebar_reference_widget_spec.js
+++ b/spec/frontend/sidebar/components/reference/sidebar_reference_widget_spec.js
@@ -1,4 +1,3 @@
-import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -8,18 +7,21 @@ import { IssuableType } from '~/issue_show/constants';
import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue';
import { issueReferenceResponse } from '../../mock_data';
describe('Sidebar Reference Widget', () => {
let wrapper;
let fakeApollo;
- const referenceText = 'reference';
+
+ const mockReferenceValue = 'reference-1234';
+
+ const findCopyableField = () => wrapper.findComponent(CopyableField);
const createComponent = ({
- issuableType,
+ issuableType = IssuableType.Issue,
referenceQuery = issueReferenceQuery,
- referenceQueryHandler = jest.fn().mockResolvedValue(issueReferenceResponse(referenceText)),
+ referenceQueryHandler = jest.fn().mockResolvedValue(issueReferenceResponse(mockReferenceValue)),
} = {}) => {
Vue.use(VueApollo);
@@ -39,14 +41,20 @@ describe('Sidebar Reference Widget', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
+ });
+
+ describe('when reference is loading', () => {
+ it('sets CopyableField `is-loading` prop to `true`', () => {
+ createComponent({ referenceQueryHandler: jest.fn().mockReturnValue(new Promise(() => {})) });
+ expect(findCopyableField().props('isLoading')).toBe(true);
+ });
});
describe.each([
[IssuableType.Issue, issueReferenceQuery],
[IssuableType.MergeRequest, mergeRequestReferenceQuery],
])('when issuableType is %s', (issuableType, referenceQuery) => {
- it('displays the reference text', async () => {
+ it('sets CopyableField `value` prop to reference value', async () => {
createComponent({
issuableType,
referenceQuery,
@@ -54,40 +62,32 @@ describe('Sidebar Reference Widget', () => {
await waitForPromises();
- expect(wrapper.text()).toContain(referenceText);
+ expect(findCopyableField().props('value')).toBe(mockReferenceValue);
});
- it('displays loading icon while fetching and hides clipboard icon', async () => {
- createComponent({
- issuableType,
- referenceQuery,
- });
+ describe('when error occurs', () => {
+ it('calls createFlash with correct parameters', async () => {
+ const mockError = new Error('mayday');
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.find(ClipboardButton).exists()).toBe(false);
- });
+ createComponent({
+ issuableType,
+ referenceQuery,
+ referenceQueryHandler: jest.fn().mockRejectedValue(mockError),
+ });
- it('calls createFlash with correct parameters', async () => {
- const mockError = new Error('mayday');
+ await waitForPromises();
- createComponent({
- issuableType,
- referenceQuery,
- referenceQueryHandler: jest.fn().mockRejectedValue(mockError),
+ const [
+ [
+ {
+ message,
+ error: { networkError },
+ },
+ ],
+ ] = wrapper.emitted('fetch-error');
+ expect(message).toBe('An error occurred while fetching reference');
+ expect(networkError).toEqual(mockError);
});
-
- await waitForPromises();
-
- const [
- [
- {
- message,
- error: { networkError },
- },
- ],
- ] = wrapper.emitted('fetch-error');
- expect(message).toBe('An error occurred while fetching reference');
- expect(networkError).toEqual(mockError);
});
});
});
diff --git a/spec/frontend/sidebar/issuable_assignees_spec.js b/spec/frontend/sidebar/issuable_assignees_spec.js
index af4dc315aad..3563d478f3f 100644
--- a/spec/frontend/sidebar/issuable_assignees_spec.js
+++ b/spec/frontend/sidebar/issuable_assignees_spec.js
@@ -5,12 +5,15 @@ import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_
describe('IssuableAssignees', () => {
let wrapper;
- const createComponent = (props = { users: [] }) => {
+ const createComponent = (props = {}) => {
wrapper = shallowMount(IssuableAssignees, {
provide: {
rootPath: '',
},
- propsData: { ...props },
+ propsData: {
+ users: [],
+ ...props,
+ },
});
};
const findUncollapsedAssigneeList = () => wrapper.find(UncollapsedAssigneeList);
@@ -22,12 +25,14 @@ describe('IssuableAssignees', () => {
});
describe('when no assignees are present', () => {
- beforeEach(() => {
- createComponent();
+ it('renders "None - assign yourself" when user is logged in', () => {
+ createComponent({ signedIn: true });
+ expect(findEmptyAssignee().text()).toBe('None - assign yourself');
});
- it('renders "None - assign yourself"', () => {
- expect(findEmptyAssignee().text()).toBe('None - assign yourself');
+ it('renders "None" when user is not logged in', () => {
+ createComponent();
+ expect(findEmptyAssignee().text()).toBe('None');
});
});
@@ -41,7 +46,7 @@ describe('IssuableAssignees', () => {
describe('when clicking "assign yourself"', () => {
it('emits "assign-self"', () => {
- createComponent();
+ createComponent({ signedIn: true });
wrapper.find('[data-testid="assign-yourself"]').vm.$emit('click');
expect(wrapper.emitted('assign-self')).toHaveLength(1);
});
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index e751f1239c8..2a4858a6320 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -233,6 +233,19 @@ export const issueConfidentialityResponse = (confidential = false) => ({
},
});
+export const issueDueDateResponse = (dueDate = null) => ({
+ data: {
+ workspace: {
+ __typename: 'Project',
+ issuable: {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/4',
+ dueDate,
+ },
+ },
+ },
+});
+
export const issueReferenceResponse = (reference) => ({
data: {
workspace: {
@@ -245,4 +258,147 @@ export const issueReferenceResponse = (reference) => ({
},
},
});
+
+export const issuableQueryResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ issuable: {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/1',
+ iid: '1',
+ participants: {
+ nodes: [
+ {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: '/root',
+ status: null,
+ },
+ {
+ id: 'gid://gitlab/User/2',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
+ name: 'Jacki Kub',
+ username: 'francina.skiles',
+ webUrl: '/franc',
+ status: {
+ availability: 'BUSY',
+ },
+ },
+ {
+ id: 'gid://gitlab/User/3',
+ avatarUrl: '/avatar',
+ name: 'John Doe',
+ username: 'johndoe',
+ webUrl: '/john',
+ status: null,
+ },
+ ],
+ },
+ assignees: {
+ nodes: [
+ {
+ id: 'gid://gitlab/User/2',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
+ name: 'Jacki Kub',
+ username: 'francina.skiles',
+ webUrl: '/franc',
+ status: null,
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+export const searchQueryResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ users: {
+ nodes: [
+ {
+ user: {
+ id: '1',
+ avatarUrl: '/avatar',
+ name: 'root',
+ username: 'root',
+ webUrl: 'root',
+ status: null,
+ },
+ },
+ {
+ user: {
+ id: '2',
+ avatarUrl: '/avatar2',
+ name: 'rookie',
+ username: 'rookie',
+ webUrl: 'rookie',
+ status: null,
+ },
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const updateIssueAssigneesMutationResponse = {
+ data: {
+ issuableSetAssignees: {
+ issuable: {
+ id: 'gid://gitlab/Issue/1',
+ iid: '1',
+ assignees: {
+ nodes: [
+ {
+ __typename: 'User',
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: '/root',
+ status: null,
+ },
+ ],
+ __typename: 'UserConnection',
+ },
+ participants: {
+ nodes: [
+ {
+ __typename: 'User',
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: '/root',
+ status: null,
+ },
+ {
+ __typename: 'User',
+ id: 'gid://gitlab/User/2',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
+ name: 'Jacki Kub',
+ username: 'francina.skiles',
+ webUrl: '/franc',
+ status: null,
+ },
+ ],
+ __typename: 'UserConnection',
+ },
+ __typename: 'Issue',
+ },
+ },
+ },
+};
+
export default mockData;