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:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-04-21 02:50:22 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-04-21 02:50:22 +0300
commit9dc93a4519d9d5d7be48ff274127136236a3adb3 (patch)
tree70467ae3692a0e35e5ea56bcb803eb512a10bedb /spec/frontend/sidebar/components
parent4b0f34b6d759d6299322b3a54453e930c6121ff0 (diff)
Add latest changes from gitlab-org/gitlab@13-11-stable-eev13.11.0-rc43
Diffstat (limited to 'spec/frontend/sidebar/components')
-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
9 files changed, 843 insertions, 53 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);
+ });
+});
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);
});
});
});