diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
commit | 4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch) | |
tree | 5423a1c7516cffe36384133ade12572cf709398d /spec/frontend/sidebar/components | |
parent | e570267f2f6b326480d284e0164a6464ba4081bc (diff) | |
download | gitlab-ce-4555e1b21c365ed8303ffb7a3325d773c9b8bf31.tar.gz |
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'spec/frontend/sidebar/components')
11 files changed, 816 insertions, 356 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 index 824f6d49c65..0e052abffeb 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js @@ -1,27 +1,20 @@ 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 getIssueAssigneesQuery from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql'; import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql'; -import { - issuableQueryResponse, - searchQueryResponse, - updateIssueAssigneesMutationResponse, -} from '../../mock_data'; +import UserSelect from '~/vue_shared/components/user_select/user_select.vue'; +import { issuableQueryResponse, updateIssueAssigneesMutationResponse } from '../../mock_data'; jest.mock('~/flash'); @@ -50,31 +43,19 @@ describe('Sidebar assignees widget', () => { 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 findUserSelect = () => wrapper.findComponent(UserSelect); 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], + [getIssueAssigneesQuery, issuableQueryHandler], [updateIssueAssigneesMutation, updateIssueAssigneesMutationHandler], ]); wrapper = shallowMount(SidebarAssigneesWidget, { @@ -82,15 +63,11 @@ describe('Sidebar assignees widget', () => { apolloProvider: fakeApollo, propsData: { iid: '1', + issuableId: 0, fullPath: '/mygroup/myProject', + allowMultipleAssignees: true, ...props, }, - data() { - return { - search, - selected: [], - }; - }, provide: { canUpdate: true, rootPath: '/', @@ -98,7 +75,7 @@ describe('Sidebar assignees widget', () => { }, stubs: { SidebarEditableItem, - MultiSelectDropdown, + UserSelect, GlSearchBoxByType, GlDropdown, }, @@ -148,19 +125,6 @@ describe('Sidebar assignees widget', () => { 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', () => { @@ -198,7 +162,7 @@ describe('Sidebar assignees widget', () => { findAssignees().vm.$emit('assign-self'); expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({ - assigneeUsernames: 'root', + assigneeUsernames: ['root'], fullPath: '/mygroup/myProject', iid: '1', }); @@ -220,7 +184,7 @@ describe('Sidebar assignees widget', () => { findAssignees().vm.$emit('assign-self'); expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({ - assigneeUsernames: 'root', + assigneeUsernames: ['root'], fullPath: '/mygroup/myProject', iid: '1', }); @@ -245,16 +209,20 @@ describe('Sidebar assignees widget', () => { ]); }); - 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'; - + it('does not trigger mutation or fire event when editing and exiting without making changes', async () => { createComponent(); + await waitForPromises(); - expandDropdown(); - expect(findCurrentUser().exists()).toBe(true); + findEditableItem().vm.$emit('open'); + + await waitForPromises(); + + findEditableItem().vm.$emit('close'); + + expect(findEditableItem().props('isDirty')).toBe(false); + expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledTimes(0); + expect(wrapper.emitted('assignees-updated')).toBe(undefined); }); describe('when expanded', () => { @@ -264,27 +232,18 @@ describe('Sidebar assignees widget', () => { expandDropdown(); }); - it('collapses the widget on multiselect dropdown toggle event', async () => { - findDropdown().vm.$emit('toggle'); + it('collapses the widget on user select toggle event', async () => { + findUserSelect().vm.$emit('toggle'); await nextTick(); - expect(findDropdown().isVisible()).toBe(false); + expect(findUserSelect().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'); + it('calls an update mutation with correct variables on User Select input event', () => { + findUserSelect().vm.$emit('input', [{ username: 'root' }]); findEditableItem().vm.$emit('close'); expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({ - assigneeUsernames: [], + assigneeUsernames: ['root'], fullPath: '/mygroup/myProject', iid: '1', }); @@ -293,68 +252,38 @@ describe('Sidebar assignees widget', () => { describe('when multiselect is disabled', () => { beforeEach(async () => { - createComponent({ props: { multipleAssignees: false } }); + createComponent({ props: { allowMultipleAssignees: false } }); await waitForPromises(); expandDropdown(); }); - it('adds a single assignee when clicking on unselected user', async () => { - findUnselectedParticipants().at(0).vm.$emit('click'); + it('closes a dropdown after User Select input event', async () => { + findUserSelect().vm.$emit('input', [{ username: 'root' }]); 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')); + await waitForPromises(); - expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({ - assigneeUsernames: [], - fullPath: '/mygroup/myProject', - iid: '1', - }); + expect(findUserSelect().isVisible()).toBe(false); }); }); describe('when multiselect is enabled', () => { beforeEach(async () => { - createComponent({ props: { multipleAssignees: true } }); + createComponent({ props: { allowMultipleAssignees: 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')); + findUserSelect().vm.$emit('input', [{ username: 'root' }]); expect(updateIssueAssigneesMutationSuccess).not.toHaveBeenCalled(); + expect(findUserSelect().isVisible()).toBe(true); }); }); @@ -363,7 +292,7 @@ describe('Sidebar assignees widget', () => { await waitForPromises(); expandDropdown(); - findUnassignLink().vm.$emit('click'); + findUserSelect().vm.$emit('input', []); findEditableItem().vm.$emit('close'); await waitForPromises(); @@ -372,95 +301,6 @@ describe('Sidebar assignees widget', () => { 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', () => { @@ -469,11 +309,6 @@ describe('Sidebar assignees widget', () => { 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); }); @@ -507,17 +342,17 @@ describe('Sidebar assignees widget', () => { expect(findEditableItem().props('isDirty')).toBe(false); }); - it('passes truthy `isDirty` prop if selected users list was changed', async () => { + it('passes truthy `isDirty` prop after User Select component emitted an input event', async () => { expandDropdown(); expect(findEditableItem().props('isDirty')).toBe(false); - findUnselectedParticipants().at(0).vm.$emit('click'); + findUserSelect().vm.$emit('input', []); 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'); + findUserSelect().vm.$emit('input', []); findEditableItem().vm.$emit('close'); await waitForPromises(); expect(findEditableItem().props('isDirty')).toBe(false); @@ -530,7 +365,7 @@ describe('Sidebar assignees widget', () => { expect(findInviteMembersLink().exists()).toBe(false); }); - it('does not render invite members link if `directlyInviteMembers` and `indirectlyInviteMembers` were not passed', async () => { + it('does not render invite members link if `directlyInviteMembers` was not passed', async () => { createComponent(); await waitForPromises(); expect(findInviteMembersLink().exists()).toBe(false); @@ -545,14 +380,4 @@ describe('Sidebar assignees widget', () => { 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_invite_members_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js index 06f7da3d1ab..cfbe7227915 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js @@ -1,25 +1,14 @@ 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, - }, - }); + const createComponent = () => { + wrapper = shallowMount(SidebarInviteMembers); }; afterEach(() => { @@ -28,32 +17,11 @@ describe('Sidebar invite members component', () => { describe('when directly inviting members', () => { beforeEach(() => { - createComponent({ directlyInviteMembers: true }); + createComponent(); }); 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/date/sidebar_date_widget_spec.js b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js new file mode 100644 index 00000000000..91cbcc6cc27 --- /dev/null +++ b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js @@ -0,0 +1,183 @@ +import { GlDatepicker } from '@gitlab/ui'; +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 SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue'; +import SidebarFormattedDate from '~/sidebar/components/date/sidebar_formatted_date.vue'; +import SidebarInheritDate from '~/sidebar/components/date/sidebar_inherit_date.vue'; +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql'; +import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql'; +import { issuableDueDateResponse, issuableStartDateResponse } from '../../mock_data'; + +jest.mock('~/flash'); + +Vue.use(VueApollo); + +describe('Sidebar date Widget', () => { + let wrapper; + let fakeApollo; + const date = '2021-04-15'; + + const findEditableItem = () => wrapper.findComponent(SidebarEditableItem); + const findPopoverIcon = () => wrapper.find('[data-testid="inherit-date-popover"]'); + const findDatePicker = () => wrapper.find(GlDatepicker); + + const createComponent = ({ + dueDateQueryHandler = jest.fn().mockResolvedValue(issuableDueDateResponse()), + startDateQueryHandler = jest.fn().mockResolvedValue(issuableStartDateResponse()), + canInherit = false, + dateType = undefined, + issuableType = 'issue', + } = {}) => { + fakeApollo = createMockApollo([ + [issueDueDateQuery, dueDateQueryHandler], + [epicStartDateQuery, startDateQueryHandler], + ]); + + wrapper = shallowMount(SidebarDateWidget, { + apolloProvider: fakeApollo, + provide: { + canUpdate: true, + }, + propsData: { + fullPath: 'group/project', + iid: '1', + issuableType, + canInherit, + dateType, + }, + stubs: { + SidebarEditableItem, + GlDatepicker, + }, + }); + }; + + 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); + }); + + it('dateType is due date by default', () => { + createComponent(); + + expect(wrapper.text()).toContain('Due date'); + }); + + it('does not display icon popover by default', () => { + createComponent(); + + expect(findPopoverIcon().exists()).toBe(false); + }); + + it('does not render GlDatePicker', () => { + createComponent(); + + expect(findDatePicker().exists()).toBe(false); + }); + + describe('when issuable has no due date', () => { + beforeEach(async () => { + createComponent({ + dueDateQueryHandler: jest.fn().mockResolvedValue(issuableDueDateResponse(null)), + }); + await waitForPromises(); + }); + + it('passes a `loading` prop as false to editable item', () => { + expect(findEditableItem().props('loading')).toBe(false); + }); + + 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(issuableDueDateResponse(date)), + }); + await waitForPromises(); + }); + + it('passes a `loading` prop as false to editable item', () => { + expect(findEditableItem().props('loading')).toBe(false); + }); + + it('emits `dueDateUpdated` event with the date payload', () => { + expect(wrapper.emitted('dueDateUpdated')).toEqual([[date]]); + }); + + it('uses a correct prop to set the initial date for GlDatePicker', () => { + expect(findDatePicker().props()).toMatchObject({ + value: null, + autocomplete: 'off', + defaultDate: expect.any(Object), + }); + }); + + it('renders GlDatePicker', async () => { + expect(findDatePicker().exists()).toBe(true); + }); + }); + + it.each` + canInherit | component | componentName | expected + ${true} | ${SidebarFormattedDate} | ${'SidebarFormattedDate'} | ${false} + ${true} | ${SidebarInheritDate} | ${'SidebarInheritDate'} | ${true} + ${false} | ${SidebarFormattedDate} | ${'SidebarFormattedDate'} | ${true} + ${false} | ${SidebarInheritDate} | ${'SidebarInheritDate'} | ${false} + `( + 'when canInherit is $canInherit, $componentName display is $expected', + ({ canInherit, component, expected }) => { + createComponent({ canInherit }); + + expect(wrapper.find(component).exists()).toBe(expected); + }, + ); + + 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(); + }); + + it.each` + dateType | text | event | mockedResponse | issuableType | queryHandler + ${'dueDate'} | ${'Due date'} | ${'dueDateUpdated'} | ${issuableDueDateResponse} | ${'issue'} | ${'dueDateQueryHandler'} + ${'startDate'} | ${'Start date'} | ${'startDateUpdated'} | ${issuableStartDateResponse} | ${'epic'} | ${'startDateQueryHandler'} + `( + 'when dateType is $dateType, component renders $text and emits $event', + async ({ dateType, text, event, mockedResponse, issuableType, queryHandler }) => { + createComponent({ + dateType, + issuableType, + [queryHandler]: jest.fn().mockResolvedValue(mockedResponse(date)), + }); + await waitForPromises(); + + expect(wrapper.text()).toContain(text); + expect(wrapper.emitted(event)).toEqual([[date]]); + }, + ); + + it('displays icon popover when issuable can inherit date', () => { + createComponent({ canInherit: true }); + + expect(findPopoverIcon().exists()).toBe(true); + }); +}); diff --git a/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js b/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js new file mode 100644 index 00000000000..1eda4ea977f --- /dev/null +++ b/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js @@ -0,0 +1,62 @@ +import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import SidebarFormattedDate from '~/sidebar/components/date/sidebar_formatted_date.vue'; + +describe('SidebarFormattedDate', () => { + let wrapper; + const findFormattedDate = () => wrapper.find("[data-testid='sidebar-date-value']"); + const findRemoveButton = () => wrapper.find(GlButton); + + const createComponent = ({ hasDate = true } = {}) => { + wrapper = shallowMount(SidebarFormattedDate, { + provide: { + canUpdate: true, + }, + propsData: { + formattedDate: 'Apr 15, 2021', + hasDate, + issuableType: 'issue', + resetText: 'remove', + isLoading: false, + canDelete: true, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('displays formatted date', () => { + expect(findFormattedDate().text()).toBe('Apr 15, 2021'); + }); + + describe('when issue has due date', () => { + it('displays remove button', () => { + expect(findRemoveButton().exists()).toBe(true); + expect(findRemoveButton().children).toEqual(wrapper.props.resetText); + }); + + it('emits reset-date event on click on remove button', () => { + findRemoveButton().vm.$emit('click'); + + expect(wrapper.emitted('reset-date')).toEqual([[undefined]]); + }); + }); + + describe('when issuable has no due date', () => { + beforeEach(() => { + createComponent({ + hasDate: false, + }); + }); + + it('does not display remove button', () => { + expect(findRemoveButton().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js b/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js new file mode 100644 index 00000000000..4d38eba8035 --- /dev/null +++ b/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js @@ -0,0 +1,53 @@ +import { GlFormRadio } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import SidebarFormattedDate from '~/sidebar/components/date/sidebar_formatted_date.vue'; +import SidebarInheritDate from '~/sidebar/components/date/sidebar_inherit_date.vue'; + +describe('SidebarInheritDate', () => { + let wrapper; + const findFixedFormattedDate = () => wrapper.findAll(SidebarFormattedDate).at(0); + const findInheritFormattedDate = () => wrapper.findAll(SidebarFormattedDate).at(1); + const findFixedRadio = () => wrapper.findAll(GlFormRadio).at(0); + const findInheritRadio = () => wrapper.findAll(GlFormRadio).at(1); + + const createComponent = () => { + wrapper = shallowMount(SidebarInheritDate, { + provide: { + canUpdate: true, + }, + propsData: { + issuable: { + dueDate: '2021-04-15', + dueDateIsFixed: true, + dueDateFixed: '2021-04-15', + dueDateFromMilestones: '2021-05-15', + }, + isLoading: false, + dateType: 'dueDate', + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('displays formatted fixed and inherited dates with radio buttons', () => { + expect(wrapper.findAll(SidebarFormattedDate)).toHaveLength(2); + expect(wrapper.findAll(GlFormRadio)).toHaveLength(2); + expect(findFixedFormattedDate().props('formattedDate')).toBe('Apr 15, 2021'); + expect(findInheritFormattedDate().props('formattedDate')).toBe('May 15, 2021'); + expect(findFixedRadio().text()).toBe('Fixed:'); + expect(findInheritRadio().text()).toBe('Inherited:'); + }); + + it('emits set-date event on click on radio button', () => { + findFixedRadio().vm.$emit('input', true); + + expect(wrapper.emitted('set-date')).toEqual([[true]]); + }); +}); 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 deleted file mode 100644 index f58ceb0f1be..00000000000 --- a/spec/frontend/sidebar/components/due_date/sidebar_due_date_widget_spec.js +++ /dev/null @@ -1,106 +0,0 @@ -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/participants/sidebar_participants_widget_spec.js b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js new file mode 100644 index 00000000000..57b9a10b23e --- /dev/null +++ b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js @@ -0,0 +1,89 @@ +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 Participants from '~/sidebar/components/participants/participants.vue'; +import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue'; +import epicParticipantsQuery from '~/sidebar/queries/epic_participants.query.graphql'; +import { epicParticipantsResponse } from '../../mock_data'; + +Vue.use(VueApollo); + +describe('Sidebar Participants Widget', () => { + let wrapper; + let fakeApollo; + + const findParticipants = () => wrapper.findComponent(Participants); + + const createComponent = ({ + participantsQueryHandler = jest.fn().mockResolvedValue(epicParticipantsResponse()), + } = {}) => { + fakeApollo = createMockApollo([[epicParticipantsQuery, participantsQueryHandler]]); + + wrapper = shallowMount(SidebarParticipantsWidget, { + apolloProvider: fakeApollo, + propsData: { + fullPath: 'group', + iid: '1', + issuableType: 'epic', + }, + stubs: { + Participants, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + fakeApollo = null; + }); + + it('passes a `loading` prop as true to child component when query is loading', () => { + createComponent(); + + expect(findParticipants().props('loading')).toBe(true); + }); + + describe('when participants are loaded', () => { + beforeEach(() => { + createComponent({ + participantsQueryHandler: jest.fn().mockResolvedValue(epicParticipantsResponse()), + }); + return waitForPromises(); + }); + + it('passes a `loading` prop as false to editable item', () => { + expect(findParticipants().props('loading')).toBe(false); + }); + + it('passes participants to child component', () => { + expect(findParticipants().props('participants')).toEqual( + epicParticipantsResponse().data.workspace.issuable.participants.nodes, + ); + }); + }); + + describe('when error occurs', () => { + it('emits error event with correct parameters', async () => { + const mockError = new Error('mayday'); + + createComponent({ + participantsQueryHandler: jest.fn().mockRejectedValue(mockError), + }); + + await waitForPromises(); + + const [ + [ + { + message, + error: { networkError }, + }, + ], + ] = wrapper.emitted('fetch-error'); + expect(message).toBe(wrapper.vm.$options.i18n.fetchingError); + expect(networkError).toEqual(mockError); + }); + }); +}); diff --git a/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js b/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js new file mode 100644 index 00000000000..549ab99c6af --- /dev/null +++ b/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js @@ -0,0 +1,131 @@ +import { GlIcon, GlToggle } from '@gitlab/ui'; +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 SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import SidebarSubscriptionWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; +import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql'; +import { issueSubscriptionsResponse } from '../../mock_data'; + +jest.mock('~/flash'); + +Vue.use(VueApollo); + +describe('Sidebar Subscriptions Widget', () => { + let wrapper; + let fakeApollo; + + const findEditableItem = () => wrapper.findComponent(SidebarEditableItem); + const findToggle = () => wrapper.findComponent(GlToggle); + const findIcon = () => wrapper.findComponent(GlIcon); + + const createComponent = ({ + subscriptionsQueryHandler = jest.fn().mockResolvedValue(issueSubscriptionsResponse()), + } = {}) => { + fakeApollo = createMockApollo([[issueSubscribedQuery, subscriptionsQueryHandler]]); + + wrapper = shallowMount(SidebarSubscriptionWidget, { + apolloProvider: fakeApollo, + provide: { + canUpdate: true, + }, + propsData: { + fullPath: 'group/project', + iid: '1', + 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 user is not subscribed to the issue', () => { + beforeEach(() => { + createComponent(); + return waitForPromises(); + }); + + it('passes a `loading` prop as false to editable item', () => { + expect(findEditableItem().props('loading')).toBe(false); + }); + + it('toggle is unchecked', () => { + expect(findToggle().props('value')).toBe(false); + }); + + it('emits `subscribedUpdated` event with a `false` payload', () => { + expect(wrapper.emitted('subscribedUpdated')).toEqual([[false]]); + }); + }); + + describe('when user is subscribed to the issue', () => { + beforeEach(() => { + createComponent({ + subscriptionsQueryHandler: jest.fn().mockResolvedValue(issueSubscriptionsResponse(true)), + }); + return waitForPromises(); + }); + + it('passes a `loading` prop as false to editable item', () => { + expect(findEditableItem().props('loading')).toBe(false); + }); + + it('toggle is checked', () => { + expect(findToggle().props('value')).toBe(true); + }); + + it('emits `subscribedUpdated` event with a `true` payload', () => { + expect(wrapper.emitted('subscribedUpdated')).toEqual([[true]]); + }); + }); + + describe('when emails are disabled', () => { + it('toggle is disabled and off when user is subscribed', async () => { + createComponent({ + subscriptionsQueryHandler: jest + .fn() + .mockResolvedValue(issueSubscriptionsResponse(true, true)), + }); + await waitForPromises(); + + expect(findIcon().props('name')).toBe('notifications-off'); + expect(findToggle().props('disabled')).toBe(true); + }); + + it('toggle is disabled and off when user is not subscribed', async () => { + createComponent({ + subscriptionsQueryHandler: jest + .fn() + .mockResolvedValue(issueSubscriptionsResponse(false, true)), + }); + await waitForPromises(); + + expect(findIcon().props('name')).toBe('notifications-off'); + expect(findToggle().props('disabled')).toBe(true); + }); + }); + + it('displays a flash message when query is rejected', async () => { + createComponent({ + subscriptionsQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'), + }); + await waitForPromises(); + + expect(createFlash).toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/sidebar/components/time_tracking/mock_data.js b/spec/frontend/sidebar/components/time_tracking/mock_data.js new file mode 100644 index 00000000000..862bcbe861e --- /dev/null +++ b/spec/frontend/sidebar/components/time_tracking/mock_data.js @@ -0,0 +1,102 @@ +export const getIssueTimelogsQueryResponse = { + data: { + issuable: { + __typename: 'Issue', + id: 'gid://gitlab/Issue/148', + title: + 'Est perferendis dicta expedita ipsum adipisci laudantium omnis consequatur consequatur et.', + timelogs: { + nodes: [ + { + __typename: 'Timelog', + timeSpent: 14400, + user: { + name: 'John Doe18', + __typename: 'UserCore', + }, + spentAt: '2020-05-01T00:00:00Z', + note: { + body: 'I paired with @root on this last week.', + __typename: 'Note', + }, + }, + { + __typename: 'Timelog', + timeSpent: 1800, + user: { + name: 'Administrator', + __typename: 'UserCore', + }, + spentAt: '2021-05-07T13:19:01Z', + note: null, + }, + { + __typename: 'Timelog', + timeSpent: 14400, + user: { + name: 'Administrator', + __typename: 'UserCore', + }, + spentAt: '2021-05-01T00:00:00Z', + note: { + body: 'I did some work on this last week.', + __typename: 'Note', + }, + }, + ], + __typename: 'TimelogConnection', + }, + }, + }, +}; + +export const getMrTimelogsQueryResponse = { + data: { + issuable: { + __typename: 'MergeRequest', + id: 'gid://gitlab/MergeRequest/29', + title: 'Esse amet perspiciatis voluptas et sed praesentium debitis repellat.', + timelogs: { + nodes: [ + { + __typename: 'Timelog', + timeSpent: 1800, + user: { + name: 'Administrator', + __typename: 'UserCore', + }, + spentAt: '2021-05-07T14:44:55Z', + note: { + body: 'Thirty minutes!', + __typename: 'Note', + }, + }, + { + __typename: 'Timelog', + timeSpent: 3600, + user: { + name: 'Administrator', + __typename: 'UserCore', + }, + spentAt: '2021-05-07T14:44:39Z', + note: null, + }, + { + __typename: 'Timelog', + timeSpent: 300, + user: { + name: 'Administrator', + __typename: 'UserCore', + }, + spentAt: '2021-03-10T00:00:00Z', + note: { + body: 'A note with some time', + __typename: 'Note', + }, + }, + ], + __typename: 'TimelogConnection', + }, + }, + }, +}; diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js new file mode 100644 index 00000000000..0aa5aa2f691 --- /dev/null +++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js @@ -0,0 +1,125 @@ +import { GlLoadingIcon } from '@gitlab/ui'; +import { getAllByRole, getByRole } from '@testing-library/dom'; +import { shallowMount, createLocalVue, mount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import Report from '~/sidebar/components/time_tracking/report.vue'; +import getIssueTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql'; +import getMrTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql'; +import { getIssueTimelogsQueryResponse, getMrTimelogsQueryResponse } from './mock_data'; + +jest.mock('~/flash'); + +describe('Issuable Time Tracking Report', () => { + const localVue = createLocalVue(); + localVue.use(VueApollo); + let wrapper; + let fakeApollo; + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const successIssueQueryHandler = jest.fn().mockResolvedValue(getIssueTimelogsQueryResponse); + const successMrQueryHandler = jest.fn().mockResolvedValue(getMrTimelogsQueryResponse); + + const mountComponent = ({ + queryHandler = successIssueQueryHandler, + issuableType = 'issue', + mountFunction = shallowMount, + limitToHours = false, + } = {}) => { + fakeApollo = createMockApollo([ + [getIssueTimelogsQuery, queryHandler], + [getMrTimelogsQuery, queryHandler], + ]); + wrapper = mountFunction(Report, { + provide: { + issuableId: 1, + issuableType, + }, + propsData: { limitToHours }, + localVue, + apolloProvider: fakeApollo, + }); + }; + + afterEach(() => { + wrapper.destroy(); + fakeApollo = null; + }); + + it('should render loading spinner', () => { + mountComponent(); + + expect(findLoadingIcon()).toExist(); + }); + + it('should render error message on reject', async () => { + mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') }); + await waitForPromises(); + + expect(createFlash).toHaveBeenCalled(); + }); + + describe('for issue', () => { + beforeEach(() => { + mountComponent({ mountFunction: mount }); + }); + + it('calls correct query', () => { + expect(successIssueQueryHandler).toHaveBeenCalled(); + }); + + it('renders correct results', async () => { + await waitForPromises(); + + expect(getAllByRole(wrapper.element, 'row', { name: /John Doe18/i })).toHaveLength(1); + expect(getAllByRole(wrapper.element, 'row', { name: /Administrator/i })).toHaveLength(2); + }); + }); + + describe('for merge request', () => { + beforeEach(() => { + mountComponent({ + queryHandler: successMrQueryHandler, + issuableType: 'merge_request', + mountFunction: mount, + }); + }); + + it('calls correct query', () => { + expect(successMrQueryHandler).toHaveBeenCalled(); + }); + + it('renders correct results', async () => { + await waitForPromises(); + + expect(getAllByRole(wrapper.element, 'row', { name: /Administrator/i })).toHaveLength(3); + }); + }); + + describe('observes `limit display of time tracking units to hours` setting', () => { + describe('when false', () => { + beforeEach(() => { + mountComponent({ limitToHours: false, mountFunction: mount }); + }); + + it('renders correct results', async () => { + await waitForPromises(); + + expect(getByRole(wrapper.element, 'columnheader', { name: /1d 30m/i })).not.toBeNull(); + }); + }); + + describe('when true', () => { + beforeEach(() => { + mountComponent({ limitToHours: true, mountFunction: mount }); + }); + + it('renders correct results', async () => { + await waitForPromises(); + + expect(getByRole(wrapper.element, 'columnheader', { name: /8h 30m/i })).not.toBeNull(); + }); + }); + }); +}); diff --git a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js index 4d03aedf1be..f26cdcb8b20 100644 --- a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js +++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js @@ -10,6 +10,7 @@ describe('Issuable Time Tracker', () => { const findComparisonMeter = () => findByTestId('compareMeter').attributes('title'); const findCollapsedState = () => findByTestId('collapsedState'); const findTimeRemainingProgress = () => findByTestId('timeRemainingProgress'); + const findReportLink = () => findByTestId('reportLink'); const defaultProps = { timeEstimate: 10_000, // 2h 46m @@ -192,6 +193,33 @@ describe('Issuable Time Tracker', () => { }); }); + describe('Time tracking report', () => { + describe('When no time spent', () => { + beforeEach(() => { + wrapper = mountComponent({ + props: { + timeSpent: 0, + timeSpentHumanReadable: '', + }, + }); + }); + + it('link should not appear', () => { + expect(findReportLink().exists()).toBe(false); + }); + }); + + describe('When time spent', () => { + beforeEach(() => { + wrapper = mountComponent(); + }); + + it('link should appear', () => { + expect(findReportLink().exists()).toBe(true); + }); + }); + }); + describe('Help pane', () => { const findHelpButton = () => findByTestId('helpButton'); const findCloseHelpButton = () => findByTestId('closeHelpButton'); |