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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/sidebar')
-rw-r--r--spec/frontend/sidebar/assignees_realtime_spec.js108
-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
-rw-r--r--spec/frontend/sidebar/mock_data.js198
-rw-r--r--spec/frontend/sidebar/sidebar_assignees_spec.js1
-rw-r--r--spec/frontend/sidebar/sidebar_subscriptions_spec.js36
15 files changed, 1010 insertions, 505 deletions
diff --git a/spec/frontend/sidebar/assignees_realtime_spec.js b/spec/frontend/sidebar/assignees_realtime_spec.js
index f0a6fa40d67..ecf33d6de37 100644
--- a/spec/frontend/sidebar/assignees_realtime_spec.js
+++ b/spec/frontend/sidebar/assignees_realtime_spec.js
@@ -1,41 +1,44 @@
-import ActionCable from '@rails/actioncable';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
-import { assigneesQueries } from '~/sidebar/constants';
+import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
import SidebarMediator from '~/sidebar/sidebar_mediator';
-import Mock from './mock_data';
+import getIssueAssigneesQuery from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
+import Mock, { issuableQueryResponse, subscriptionNullResponse } from './mock_data';
-jest.mock('@rails/actioncable', () => {
- const mockConsumer = {
- subscriptions: { create: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }) },
- };
- return {
- createConsumer: jest.fn().mockReturnValue(mockConsumer),
- };
-});
+const localVue = createLocalVue();
+localVue.use(VueApollo);
describe('Assignees Realtime', () => {
let wrapper;
let mediator;
+ let fakeApollo;
+
+ const issuableQueryHandler = jest.fn().mockResolvedValue(issuableQueryResponse);
+ const subscriptionInitialHandler = jest.fn().mockResolvedValue(subscriptionNullResponse);
- const createComponent = (issuableType = 'issue') => {
+ const createComponent = ({
+ issuableType = 'issue',
+ issuableId = 1,
+ subscriptionHandler = subscriptionInitialHandler,
+ } = {}) => {
+ fakeApollo = createMockApollo([
+ [getIssueAssigneesQuery, issuableQueryHandler],
+ [issuableAssigneesSubscription, subscriptionHandler],
+ ]);
wrapper = shallowMount(AssigneesRealtime, {
propsData: {
- issuableIid: '1',
- mediator,
- projectPath: 'path/to/project',
issuableType,
- },
- mocks: {
- $apollo: {
- query: assigneesQueries[issuableType].query,
- queries: {
- workspace: {
- refetch: jest.fn(),
- },
- },
+ issuableId,
+ queryVariables: {
+ issuableIid: '1',
+ projectPath: 'path/to/project',
},
+ mediator,
},
+ apolloProvider: fakeApollo,
+ localVue,
});
};
@@ -45,59 +48,24 @@ describe('Assignees Realtime', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
+ fakeApollo = null;
SidebarMediator.singleton = null;
});
- describe('when handleFetchResult is called from smart query', () => {
- it('sets assignees to the store', () => {
- const data = {
- workspace: {
- issuable: {
- assignees: {
- nodes: [{ id: 'gid://gitlab/Environments/123', avatarUrl: 'url' }],
- },
- },
- },
- };
- const expected = [{ id: 123, avatar_url: 'url', avatarUrl: 'url' }];
- createComponent();
+ it('calls the query with correct variables', () => {
+ createComponent();
- wrapper.vm.handleFetchResult({ data });
-
- expect(mediator.store.assignees).toEqual(expected);
+ expect(issuableQueryHandler).toHaveBeenCalledWith({
+ issuableIid: '1',
+ projectPath: 'path/to/project',
});
});
- describe('when mounted', () => {
- it('calls create subscription', () => {
- const cable = ActionCable.createConsumer();
-
- createComponent();
-
- return wrapper.vm.$nextTick().then(() => {
- expect(cable.subscriptions.create).toHaveBeenCalledTimes(1);
- expect(cable.subscriptions.create).toHaveBeenCalledWith(
- {
- channel: 'IssuesChannel',
- iid: wrapper.props('issuableIid'),
- project_path: wrapper.props('projectPath'),
- },
- { received: wrapper.vm.received },
- );
- });
- });
- });
-
- describe('when subscription is recieved', () => {
- it('refetches the GraphQL project query', () => {
- createComponent();
-
- wrapper.vm.received({ event: 'updated' });
+ it('calls the subscription with correct variable for issue', () => {
+ createComponent();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.$apollo.queries.workspace.refetch).toHaveBeenCalledTimes(1);
- });
+ expect(subscriptionInitialHandler).toHaveBeenCalledWith({
+ issuableId: 'gid://gitlab/Issue/1',
});
});
});
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
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');
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index 2a4858a6320..b052038661a 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -233,7 +233,7 @@ export const issueConfidentialityResponse = (confidential = false) => ({
},
});
-export const issueDueDateResponse = (dueDate = null) => ({
+export const issuableDueDateResponse = (dueDate = null) => ({
data: {
workspace: {
__typename: 'Project',
@@ -246,59 +246,82 @@ export const issueDueDateResponse = (dueDate = null) => ({
},
});
-export const issueReferenceResponse = (reference) => ({
+export const issuableStartDateResponse = (startDate = null) => ({
data: {
workspace: {
- __typename: 'Project',
+ __typename: 'Group',
issuable: {
- __typename: 'Issue',
- id: 'gid://gitlab/Issue/4',
- reference,
+ __typename: 'Epic',
+ id: 'gid://gitlab/Epic/4',
+ startDate,
+ startDateIsFixed: true,
+ startDateFixed: startDate,
+ startDateFromMilestones: null,
},
},
},
});
-export const issuableQueryResponse = {
+export const epicParticipantsResponse = () => ({
data: {
workspace: {
- __typename: 'Project',
+ __typename: 'Group',
issuable: {
- __typename: 'Issue',
- id: 'gid://gitlab/Issue/1',
- iid: '1',
+ __typename: 'Epic',
+ id: 'gid://gitlab/Epic/4',
participants: {
nodes: [
{
- id: 'gid://gitlab/User/1',
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- name: 'Administrator',
- username: 'root',
- webUrl: '/root',
- status: null,
- },
- {
id: 'gid://gitlab/User/2',
avatarUrl:
'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
name: 'Jacki Kub',
username: 'francina.skiles',
webUrl: '/franc',
- status: {
- availability: 'BUSY',
- },
- },
- {
- id: 'gid://gitlab/User/3',
- avatarUrl: '/avatar',
- name: 'John Doe',
- username: 'johndoe',
- webUrl: '/john',
status: null,
},
],
},
+ },
+ },
+ },
+});
+
+export const issueReferenceResponse = (reference) => ({
+ data: {
+ workspace: {
+ __typename: 'Project',
+ issuable: {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/4',
+ reference,
+ },
+ },
+ },
+});
+
+export const issueSubscriptionsResponse = (subscribed = false, emailsDisabled = false) => ({
+ data: {
+ workspace: {
+ __typename: 'Project',
+ issuable: {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/4',
+ subscribed,
+ emailsDisabled,
+ },
+ },
+ },
+});
+
+export const issuableQueryResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ issuable: {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/1',
+ iid: '1',
assignees: {
nodes: [
{
@@ -370,32 +393,121 @@ export const updateIssueAssigneesMutationResponse = {
],
__typename: 'UserConnection',
},
- participants: {
- nodes: [
- {
- __typename: 'User',
- id: 'gid://gitlab/User/1',
+ __typename: 'Issue',
+ },
+ },
+ },
+};
+
+export const subscriptionNullResponse = {
+ data: {
+ issuableAssigneesUpdated: null,
+ },
+};
+
+const mockUser1 = {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: '/root',
+ status: null,
+};
+
+const mockUser2 = {
+ id: 'gid://gitlab/User/4',
+ avatarUrl: '/avatar2',
+ name: 'rookie',
+ username: 'rookie',
+ webUrl: 'rookie',
+ status: null,
+};
+
+export const searchResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ users: {
+ nodes: [
+ {
+ user: mockUser1,
+ },
+ {
+ user: mockUser2,
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const projectMembersResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ users: {
+ nodes: [
+ // Remove nulls https://gitlab.com/gitlab-org/gitlab/-/issues/329750
+ null,
+ null,
+ // Remove duplicated entry https://gitlab.com/gitlab-org/gitlab/-/issues/327822
+ mockUser1,
+ mockUser1,
+ mockUser2,
+ {
+ user: {
+ id: 'gid://gitlab/User/2',
avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- name: 'Administrator',
- username: 'root',
- webUrl: '/root',
- status: null,
+ 'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
+ name: 'Jacki Kub',
+ username: 'francina.skiles',
+ webUrl: '/franc',
+ status: {
+ availability: 'BUSY',
+ },
},
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const participantsQueryResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ issuable: {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/1',
+ iid: '1',
+ participants: {
+ nodes: [
+ // Remove duplicated entry https://gitlab.com/gitlab-org/gitlab/-/issues/327822
+ mockUser1,
+ mockUser1,
{
- __typename: 'User',
id: 'gid://gitlab/User/2',
avatarUrl:
'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
name: 'Jacki Kub',
username: 'francina.skiles',
webUrl: '/franc',
+ status: {
+ availability: 'BUSY',
+ },
+ },
+ {
+ id: 'gid://gitlab/User/3',
+ avatarUrl: '/avatar',
+ name: 'John Doe',
+ username: 'rollie',
+ webUrl: '/john',
status: null,
},
],
- __typename: 'UserConnection',
},
- __typename: 'Issue',
},
},
},
diff --git a/spec/frontend/sidebar/sidebar_assignees_spec.js b/spec/frontend/sidebar/sidebar_assignees_spec.js
index e737b57e33d..dc121dcb897 100644
--- a/spec/frontend/sidebar/sidebar_assignees_spec.js
+++ b/spec/frontend/sidebar/sidebar_assignees_spec.js
@@ -17,6 +17,7 @@ describe('sidebar assignees', () => {
wrapper = shallowMount(SidebarAssignees, {
propsData: {
issuableIid: '1',
+ issuableId: 1,
mediator,
field: '',
projectPath: 'projectPath',
diff --git a/spec/frontend/sidebar/sidebar_subscriptions_spec.js b/spec/frontend/sidebar/sidebar_subscriptions_spec.js
deleted file mode 100644
index d900fde7e70..00000000000
--- a/spec/frontend/sidebar/sidebar_subscriptions_spec.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import SidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue';
-import SidebarService from '~/sidebar/services/sidebar_service';
-import SidebarMediator from '~/sidebar/sidebar_mediator';
-import SidebarStore from '~/sidebar/stores/sidebar_store';
-import Mock from './mock_data';
-
-describe('Sidebar Subscriptions', () => {
- let wrapper;
- let mediator;
-
- beforeEach(() => {
- mediator = new SidebarMediator(Mock.mediator);
- wrapper = shallowMount(SidebarSubscriptions, {
- propsData: {
- mediator,
- },
- });
- });
-
- afterEach(() => {
- wrapper.destroy();
- SidebarService.singleton = null;
- SidebarStore.singleton = null;
- SidebarMediator.singleton = null;
- });
-
- it('calls the mediator toggleSubscription on event', () => {
- const spy = jest.spyOn(mediator, 'toggleSubscription').mockReturnValue(Promise.resolve());
-
- wrapper.vm.onToggleSubscription();
-
- expect(spy).toHaveBeenCalled();
- spy.mockRestore();
- });
-});