summaryrefslogtreecommitdiff
path: root/spec/frontend/sidebar/components
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 15:44:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 15:44:42 +0000
commit4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch)
tree5423a1c7516cffe36384133ade12572cf709398d /spec/frontend/sidebar/components
parente570267f2f6b326480d284e0164a6464ba4081bc (diff)
downloadgitlab-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')
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js255
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js38
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js183
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js62
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js53
-rw-r--r--spec/frontend/sidebar/components/due_date/sidebar_due_date_widget_spec.js106
-rw-r--r--spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js89
-rw-r--r--spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js131
-rw-r--r--spec/frontend/sidebar/components/time_tracking/mock_data.js102
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js125
-rw-r--r--spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js28
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');