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_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js11
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js7
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js503
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js2
-rw-r--r--spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js131
-rw-r--r--spec/frontend/sidebar/mock_data.js96
-rw-r--r--spec/frontend/sidebar/track_invite_members_spec.js37
8 files changed, 753 insertions, 38 deletions
diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/assignees_spec.js
index 74dce499999..be27a800418 100644
--- a/spec/frontend/sidebar/assignees_spec.js
+++ b/spec/frontend/sidebar/assignees_spec.js
@@ -19,7 +19,7 @@ describe('Assignee component', () => {
});
};
- const findComponentTextNoUsers = () => wrapper.find('.assign-yourself');
+ const findComponentTextNoUsers = () => wrapper.find('[data-testid="no-value"]');
const findCollapsedChildren = () => wrapper.findAll('.sidebar-collapsed-icon > *');
afterEach(() => {
@@ -64,7 +64,7 @@ describe('Assignee component', () => {
});
jest.spyOn(wrapper.vm, '$emit');
- wrapper.find('.assign-yourself .btn-link').trigger('click');
+ wrapper.find('[data-testid="assign-yourself"]').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('assign-self')).toBeTruthy();
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 cfbe7227915..b738d931040 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
@@ -4,11 +4,16 @@ import SidebarInviteMembers from '~/sidebar/components/assignees/sidebar_invite_
describe('Sidebar invite members component', () => {
let wrapper;
+ const issuableType = 'issue';
const findDirectInviteLink = () => wrapper.findComponent(InviteMembersTrigger);
const createComponent = () => {
- wrapper = shallowMount(SidebarInviteMembers);
+ wrapper = shallowMount(SidebarInviteMembers, {
+ propsData: {
+ issuableType,
+ },
+ });
};
afterEach(() => {
@@ -23,5 +28,9 @@ describe('Sidebar invite members component', () => {
it('renders a direct link to project members path', () => {
expect(findDirectInviteLink().exists()).toBe(true);
});
+
+ it('has expected attributes on the trigger', () => {
+ expect(findDirectInviteLink().props('triggerSource')).toBe('issue-assignee-dropdown');
+ });
});
});
diff --git a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
index 91cbcc6cc27..619e89beb23 100644
--- a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
+++ b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
@@ -22,6 +22,10 @@ describe('Sidebar date Widget', () => {
let fakeApollo;
const date = '2021-04-15';
+ window.gon = {
+ first_day_of_week: 1,
+ };
+
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findPopoverIcon = () => wrapper.find('[data-testid="inherit-date-popover"]');
const findDatePicker = () => wrapper.find(GlDatepicker);
@@ -119,11 +123,12 @@ describe('Sidebar date Widget', () => {
expect(wrapper.emitted('dueDateUpdated')).toEqual([[date]]);
});
- it('uses a correct prop to set the initial date for GlDatePicker', () => {
+ it('uses a correct prop to set the initial date and first day of the week for GlDatePicker', () => {
expect(findDatePicker().props()).toMatchObject({
value: null,
autocomplete: 'off',
defaultDate: expect.any(Object),
+ firstDay: window.gon.first_day_of_week,
});
});
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
new file mode 100644
index 00000000000..8d58854b013
--- /dev/null
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
@@ -0,0 +1,503 @@
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlLink,
+ GlSearchBoxByType,
+ GlFormInput,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { IssuableType } from '~/issue_show/constants';
+import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+import { IssuableAttributeType } from '~/sidebar/constants';
+import projectIssueMilestoneMutation from '~/sidebar/queries/project_issue_milestone.mutation.graphql';
+import projectIssueMilestoneQuery from '~/sidebar/queries/project_issue_milestone.query.graphql';
+import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
+
+import {
+ mockIssue,
+ mockProjectMilestonesResponse,
+ noCurrentMilestoneResponse,
+ mockMilestoneMutationResponse,
+ mockMilestone2,
+ emptyProjectMilestonesResponse,
+} from '../mock_data';
+
+jest.mock('~/flash');
+
+const localVue = createLocalVue();
+
+describe('SidebarDropdownWidget', () => {
+ let wrapper;
+ let mockApollo;
+
+ const promiseData = { issuableSetAttribute: { issue: { attribute: { id: '123' } } } };
+ const firstErrorMsg = 'first error';
+ const promiseWithErrors = {
+ ...promiseData,
+ issuableSetAttribute: { ...promiseData.issuableSetAttribute, errors: [firstErrorMsg] },
+ };
+
+ const mutationSuccess = () => jest.fn().mockResolvedValue({ data: promiseData });
+ const mutationError = () =>
+ jest.fn().mockRejectedValue('Failed to set milestone on this issue. Please try again.');
+ const mutationSuccessWithErrors = () => jest.fn().mockResolvedValue({ data: promiseWithErrors });
+
+ const findGlLink = () => wrapper.findComponent(GlLink);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownText = () => wrapper.findComponent(GlDropdownText);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdownItemWithText = (text) =>
+ findAllDropdownItems().wrappers.find((x) => x.text() === text);
+
+ const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
+ const findEditButton = () => findSidebarEditableItem().find('[data-testid="edit-button"]');
+ const findEditableLoadingIcon = () => findSidebarEditableItem().findComponent(GlLoadingIcon);
+ const findAttributeItems = () => wrapper.findByTestId('milestone-items');
+ const findSelectedAttribute = () => wrapper.findByTestId('select-milestone');
+ const findNoAttributeItem = () => wrapper.findByTestId('no-milestone-item');
+ const findLoadingIconDropdown = () => wrapper.findByTestId('loading-icon-dropdown');
+
+ const waitForDropdown = async () => {
+ // BDropdown first changes its `visible` property
+ // in a requestAnimationFrame callback.
+ // It then emits `shown` event in a watcher for `visible`
+ // Hence we need both of these:
+ await waitForPromises();
+ await wrapper.vm.$nextTick();
+ };
+
+ const waitForApollo = async () => {
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+ };
+
+ // Used with createComponentWithApollo which uses 'mount'
+ const clickEdit = async () => {
+ await findEditButton().trigger('click');
+
+ await waitForDropdown();
+
+ // We should wait for attributes list to be fetched.
+ await waitForApollo();
+ };
+
+ // Used with createComponent which shallow mounts components
+ const toggleDropdown = async () => {
+ wrapper.vm.$refs.editable.expand();
+
+ await waitForDropdown();
+ };
+
+ const createComponentWithApollo = async ({
+ requestHandlers = [],
+ projectMilestonesSpy = jest.fn().mockResolvedValue(mockProjectMilestonesResponse),
+ currentMilestoneSpy = jest.fn().mockResolvedValue(noCurrentMilestoneResponse),
+ } = {}) => {
+ localVue.use(VueApollo);
+ mockApollo = createMockApollo([
+ [projectMilestonesQuery, projectMilestonesSpy],
+ [projectIssueMilestoneQuery, currentMilestoneSpy],
+ ...requestHandlers,
+ ]);
+
+ wrapper = extendedWrapper(
+ mount(SidebarDropdownWidget, {
+ localVue,
+ provide: { canUpdate: true },
+ apolloProvider: mockApollo,
+ propsData: {
+ workspacePath: mockIssue.projectPath,
+ attrWorkspacePath: mockIssue.projectPath,
+ iid: mockIssue.iid,
+ issuableType: IssuableType.Issue,
+ issuableAttribute: IssuableAttributeType.Milestone,
+ },
+ attachTo: document.body,
+ }),
+ );
+
+ await waitForApollo();
+ };
+
+ const createComponent = ({ data = {}, mutationPromise = mutationSuccess, queries = {} } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(SidebarDropdownWidget, {
+ provide: { canUpdate: true },
+ data() {
+ return data;
+ },
+ propsData: {
+ workspacePath: '',
+ attrWorkspacePath: '',
+ iid: '',
+ issuableType: IssuableType.Issue,
+ issuableAttribute: IssuableAttributeType.Milestone,
+ },
+ mocks: {
+ $apollo: {
+ mutate: mutationPromise(),
+ queries: {
+ currentAttribute: { loading: false },
+ attributesList: { loading: false },
+ ...queries,
+ },
+ },
+ },
+ stubs: {
+ SidebarEditableItem,
+ GlSearchBoxByType,
+ GlDropdown,
+ },
+ }),
+ );
+
+ // We need to mock out `showDropdown` which
+ // invokes `show` method of BDropdown used inside GlDropdown.
+ jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when not editing', () => {
+ beforeEach(() => {
+ createComponent({
+ data: {
+ currentAttribute: { id: 'id', title: 'title', webUrl: 'webUrl' },
+ },
+ stubs: {
+ GlDropdown,
+ SidebarEditableItem,
+ },
+ });
+ });
+
+ it('shows the current attribute', () => {
+ expect(findSelectedAttribute().text()).toBe('title');
+ });
+
+ it('links to the current attribute', () => {
+ expect(findGlLink().attributes().href).toBe('webUrl');
+ });
+
+ it('does not show a loading spinner next to the heading', () => {
+ expect(findEditableLoadingIcon().exists()).toBe(false);
+ });
+
+ it('shows a loading spinner while fetching the current attribute', () => {
+ createComponent({
+ queries: {
+ currentAttribute: { loading: true },
+ },
+ });
+
+ expect(findEditableLoadingIcon().exists()).toBe(true);
+ });
+
+ it('shows the loading spinner and the title of the selected attribute while updating', () => {
+ createComponent({
+ data: {
+ updating: true,
+ selectedTitle: 'Some milestone title',
+ },
+ queries: {
+ currentAttribute: { loading: false },
+ },
+ });
+
+ expect(findEditableLoadingIcon().exists()).toBe(true);
+ expect(findSelectedAttribute().text()).toBe('Some milestone title');
+ });
+
+ describe('when current attribute does not exist', () => {
+ it('renders "None" as the selected attribute title', () => {
+ createComponent();
+
+ expect(findSelectedAttribute().text()).toBe('None');
+ });
+ });
+ });
+
+ describe('when a user can edit', () => {
+ describe('when user is editing', () => {
+ describe('when rendering the dropdown', () => {
+ it('shows a loading spinner while fetching a list of attributes', async () => {
+ createComponent({
+ queries: {
+ attributesList: { loading: true },
+ },
+ });
+
+ await toggleDropdown();
+
+ expect(findLoadingIconDropdown().exists()).toBe(true);
+ });
+
+ describe('GlDropdownItem with the right title and id', () => {
+ const id = 'id';
+ const title = 'title';
+
+ beforeEach(async () => {
+ createComponent({
+ data: { attributesList: [{ id, title }], currentAttribute: { id, title } },
+ });
+
+ await toggleDropdown();
+ });
+
+ it('does not show a loading spinner', () => {
+ expect(findLoadingIconDropdown().exists()).toBe(false);
+ });
+
+ it('renders title $title', () => {
+ expect(findDropdownItemWithText(title).exists()).toBe(true);
+ });
+
+ it('checks the correct dropdown item', () => {
+ expect(
+ findAllDropdownItems()
+ .filter((w) => w.props('isChecked') === true)
+ .at(0)
+ .text(),
+ ).toBe(title);
+ });
+ });
+
+ describe('when no data is assigned', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await toggleDropdown();
+ });
+
+ it('finds GlDropdownItem with "No milestone"', () => {
+ expect(findNoAttributeItem().text()).toBe('No milestone');
+ });
+
+ it('"No milestone" is checked', () => {
+ expect(findNoAttributeItem().props('isChecked')).toBe(true);
+ });
+
+ it('does not render any dropdown item', () => {
+ expect(findAttributeItems().exists()).toBe(false);
+ });
+ });
+
+ describe('when clicking on dropdown item', () => {
+ describe('when currentAttribute is equal to attribute id', () => {
+ it('does not call setIssueAttribute mutation', async () => {
+ createComponent({
+ data: {
+ attributesList: [{ id: 'id', title: 'title' }],
+ currentAttribute: { id: 'id', title: 'title' },
+ },
+ });
+
+ await toggleDropdown();
+
+ findDropdownItemWithText('title').vm.$emit('click');
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe('when currentAttribute is not equal to attribute id', () => {
+ describe('when error', () => {
+ const bootstrapComponent = (mutationResp) => {
+ createComponent({
+ data: {
+ attributesList: [
+ { id: '123', title: '123' },
+ { id: 'id', title: 'title' },
+ ],
+ currentAttribute: '123',
+ },
+ mutationPromise: mutationResp,
+ });
+ };
+
+ describe.each`
+ description | mutationResp | expectedMsg
+ ${'top-level error'} | ${mutationError} | ${'Failed to set milestone on this issue. Please try again.'}
+ ${'user-recoverable error'} | ${mutationSuccessWithErrors} | ${firstErrorMsg}
+ `(`$description`, ({ mutationResp, expectedMsg }) => {
+ beforeEach(async () => {
+ bootstrapComponent(mutationResp);
+
+ await toggleDropdown();
+
+ findDropdownItemWithText('title').vm.$emit('click');
+ });
+
+ it(`calls createFlash with "${expectedMsg}"`, async () => {
+ await wrapper.vm.$nextTick();
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expectedMsg,
+ captureError: true,
+ error: expectedMsg,
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('when a user is searching', () => {
+ describe('when search result is not found', () => {
+ it('renders "No milestone found"', async () => {
+ createComponent();
+
+ await toggleDropdown();
+
+ findSearchBox().vm.$emit('input', 'non existing milestones');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findDropdownText().text()).toBe('No milestone found');
+ });
+ });
+ });
+ });
+ });
+
+ describe('with mock apollo', () => {
+ let error;
+
+ beforeEach(() => {
+ jest.spyOn(Sentry, 'captureException');
+ error = new Error('mayday');
+ });
+
+ describe("when issuable type is 'issue'", () => {
+ describe('when dropdown is expanded and user can edit', () => {
+ let milestoneMutationSpy;
+ beforeEach(async () => {
+ milestoneMutationSpy = jest.fn().mockResolvedValue(mockMilestoneMutationResponse);
+
+ await createComponentWithApollo({
+ requestHandlers: [[projectIssueMilestoneMutation, milestoneMutationSpy]],
+ });
+
+ await clickEdit();
+ });
+
+ it('renders the dropdown on clicking edit', async () => {
+ expect(findDropdown().isVisible()).toBe(true);
+ });
+
+ it('focuses on the input when dropdown is shown', async () => {
+ expect(document.activeElement).toEqual(wrapper.findComponent(GlFormInput).element);
+ });
+
+ describe('when currentAttribute is not equal to attribute id', () => {
+ describe('when update is successful', () => {
+ beforeEach(() => {
+ findDropdownItemWithText(mockMilestone2.title).vm.$emit('click');
+ });
+
+ it('calls setIssueAttribute mutation', () => {
+ expect(milestoneMutationSpy).toHaveBeenCalledWith({
+ iid: mockIssue.iid,
+ attributeId: getIdFromGraphQLId(mockMilestone2.id),
+ fullPath: mockIssue.projectPath,
+ });
+ });
+
+ it('sets the value returned from the mutation to currentAttribute', async () => {
+ expect(findSelectedAttribute().text()).toBe(mockMilestone2.title);
+ });
+ });
+ });
+
+ describe('milestones', () => {
+ let projectMilestonesSpy;
+
+ it('should call createFlash if milestones query fails', async () => {
+ await createComponentWithApollo({
+ projectMilestonesSpy: jest.fn().mockRejectedValue(error),
+ });
+
+ await clickEdit();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: wrapper.vm.i18n.listFetchError,
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+
+ it('only fetches attributes when dropdown is opened', async () => {
+ projectMilestonesSpy = jest.fn().mockResolvedValueOnce(emptyProjectMilestonesResponse);
+ await createComponentWithApollo({ projectMilestonesSpy });
+
+ expect(projectMilestonesSpy).not.toHaveBeenCalled();
+
+ await clickEdit();
+
+ expect(projectMilestonesSpy).toHaveBeenNthCalledWith(1, {
+ fullPath: mockIssue.projectPath,
+ title: '',
+ state: 'active',
+ });
+ });
+
+ describe('when a user is searching', () => {
+ const mockSearchTerm = 'foobar';
+
+ beforeEach(async () => {
+ projectMilestonesSpy = jest
+ .fn()
+ .mockResolvedValueOnce(emptyProjectMilestonesResponse);
+ await createComponentWithApollo({ projectMilestonesSpy });
+
+ await clickEdit();
+ });
+
+ it('sends a projectMilestones query with the entered search term "foo"', async () => {
+ findSearchBox().vm.$emit('input', mockSearchTerm);
+ await wrapper.vm.$nextTick();
+
+ // Account for debouncing
+ jest.runAllTimers();
+
+ expect(projectMilestonesSpy).toHaveBeenNthCalledWith(2, {
+ fullPath: mockIssue.projectPath,
+ title: mockSearchTerm,
+ state: 'active',
+ });
+ });
+ });
+ });
+ });
+
+ describe('currentAttributes', () => {
+ it('should call createFlash if currentAttributes query fails', async () => {
+ await createComponentWithApollo({
+ currentMilestoneSpy: jest.fn().mockRejectedValue(error),
+ });
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: wrapper.vm.i18n.currentFetchError,
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js
index 0aa5aa2f691..710fae8ddf7 100644
--- a/spec/frontend/sidebar/components/time_tracking/report_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js
@@ -36,7 +36,7 @@ describe('Issuable Time Tracking Report', () => {
issuableId: 1,
issuableType,
},
- propsData: { limitToHours },
+ propsData: { limitToHours, issuableId: '1' },
localVue,
apolloProvider: fakeApollo,
});
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 f26cdcb8b20..e08bd80b18e 100644
--- a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
@@ -1,7 +1,11 @@
import { mount } from '@vue/test-utils';
+
import { stubTransition } from 'helpers/stub_transition';
import { createMockDirective } from 'helpers/vue_mock_directive';
import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
+import SidebarEventHub from '~/sidebar/event_hub';
+
+import { issuableTimeTrackingResponse } from '../../mock_data';
describe('Issuable Time Tracker', () => {
let wrapper;
@@ -13,21 +17,39 @@ describe('Issuable Time Tracker', () => {
const findReportLink = () => findByTestId('reportLink');
const defaultProps = {
- timeEstimate: 10_000, // 2h 46m
- timeSpent: 5_000, // 1h 23m
- humanTimeEstimate: '2h 46m',
- humanTimeSpent: '1h 23m',
limitToHours: false,
+ fullPath: 'gitlab-org/gitlab-test',
+ issuableIid: '1',
+ initialTimeTracking: {
+ ...issuableTimeTrackingResponse.data.workspace.issuable,
+ },
};
- const mountComponent = ({ props = {} } = {}) =>
- mount(TimeTracker, {
+ const issuableTimeTrackingRefetchSpy = jest.fn();
+
+ const mountComponent = ({ props = {}, issuableType = 'issue', loading = false } = {}) => {
+ return mount(TimeTracker, {
propsData: { ...defaultProps, ...props },
directives: { GlTooltip: createMockDirective() },
stubs: {
transition: stubTransition(),
},
+ provide: {
+ issuableType,
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ issuableTimeTracking: {
+ loading,
+ refetch: issuableTimeTrackingRefetchSpy,
+ query: jest.fn().mockResolvedValue(issuableTimeTrackingResponse),
+ },
+ },
+ },
+ },
});
+ };
afterEach(() => {
wrapper.destroy();
@@ -44,13 +66,13 @@ describe('Issuable Time Tracker', () => {
it('should correctly render timeEstimate', () => {
expect(findByTestId('timeTrackingComparisonPane').html()).toContain(
- defaultProps.humanTimeEstimate,
+ defaultProps.initialTimeTracking.humanTimeEstimate,
);
});
- it('should correctly render time_spent', () => {
+ it('should correctly render totalTimeSpent', () => {
expect(findByTestId('timeTrackingComparisonPane').html()).toContain(
- defaultProps.humanTimeSpent,
+ defaultProps.initialTimeTracking.humanTotalTimeSpent,
);
});
});
@@ -78,10 +100,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => {
wrapper = mountComponent({
props: {
- timeEstimate: 100_000, // 1d 3h
- timeSpent: 5_000, // 1h 23m
- humanTimeEstimate: '1d 3h',
- humanTimeSpent: '1h 23m',
+ initialTimeTracking: {
+ timeEstimate: 100_000, // 1d 3h
+ totalTimeSpent: 5_000, // 1h 23m
+ humanTimeEstimate: '1d 3h',
+ humanTotalTimeSpent: '1h 23m',
+ },
},
});
});
@@ -108,8 +132,11 @@ describe('Issuable Time Tracker', () => {
it('should display the remaining meter with the correct background color when over estimate', () => {
wrapper = mountComponent({
props: {
- timeEstimate: 10_000, // 2h 46m
- timeSpent: 20_000_000, // 231 days
+ initialTimeTracking: {
+ ...defaultProps.initialTimeTracking,
+ timeEstimate: 10_000, // 2h 46m
+ totalTimeSpent: 20_000_000, // 231 days
+ },
},
});
@@ -122,8 +149,11 @@ describe('Issuable Time Tracker', () => {
beforeEach(async () => {
wrapper = mountComponent({
props: {
- timeEstimate: 100_000, // 1d 3h
limitToHours: true,
+ initialTimeTracking: {
+ ...defaultProps.initialTimeTracking,
+ timeEstimate: 100_000, // 1d 3h
+ },
},
});
});
@@ -140,10 +170,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(async () => {
wrapper = mountComponent({
props: {
- timeEstimate: 10_000, // 2h 46m
- timeSpent: 0,
- timeEstimateHumanReadable: '2h 46m',
- timeSpentHumanReadable: '',
+ initialTimeTracking: {
+ timeEstimate: 10_000, // 2h 46m
+ totalTimeSpent: 0,
+ humanTimeEstimate: '2h 46m',
+ humanTotalTimeSpent: '',
+ },
},
});
await wrapper.vm.$nextTick();
@@ -159,10 +191,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => {
wrapper = mountComponent({
props: {
- timeEstimate: 0,
- timeSpent: 5_000, // 1h 23m
- timeEstimateHumanReadable: '2h 46m',
- timeSpentHumanReadable: '1h 23m',
+ initialTimeTracking: {
+ timeEstimate: 0,
+ totalTimeSpent: 5_000, // 1h 23m
+ humanTimeEstimate: '2h 46m',
+ humanTotalTimeSpent: '1h 23m',
+ },
},
});
});
@@ -177,10 +211,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => {
wrapper = mountComponent({
props: {
- timeEstimate: 0,
- timeSpent: 0,
- timeEstimateHumanReadable: '',
- timeSpentHumanReadable: '',
+ initialTimeTracking: {
+ timeEstimate: 0,
+ totalTimeSpent: 0,
+ humanTimeEstimate: '',
+ humanTotalTimeSpent: '',
+ },
},
});
});
@@ -198,8 +234,11 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => {
wrapper = mountComponent({
props: {
- timeSpent: 0,
- timeSpentHumanReadable: '',
+ initialTimeTracking: {
+ ...defaultProps.initialTimeTracking,
+ totalTimeSpent: 0,
+ humanTotalTimeSpent: '',
+ },
},
});
});
@@ -210,13 +249,20 @@ describe('Issuable Time Tracker', () => {
});
describe('When time spent', () => {
- beforeEach(() => {
+ it('link should appear on issue', () => {
wrapper = mountComponent();
+ expect(findReportLink().exists()).toBe(true);
});
- it('link should appear', () => {
+ it('link should appear on merge request', () => {
+ wrapper = mountComponent({ issuableType: 'merge_request' });
expect(findReportLink().exists()).toBe(true);
});
+
+ it('link should not appear on milestone', () => {
+ wrapper = mountComponent({ issuableType: 'milestone' });
+ expect(findReportLink().exists()).toBe(false);
+ });
});
});
@@ -225,7 +271,16 @@ describe('Issuable Time Tracker', () => {
const findCloseHelpButton = () => findByTestId('closeHelpButton');
beforeEach(async () => {
- wrapper = mountComponent({ props: { timeEstimate: 0, timeSpent: 0 } });
+ wrapper = mountComponent({
+ props: {
+ initialTimeTracking: {
+ timeEstimate: 0,
+ totalTimeSpent: 0,
+ humanTimeEstimate: '',
+ humanTotalTimeSpent: '',
+ },
+ },
+ });
await wrapper.vm.$nextTick();
});
@@ -254,4 +309,14 @@ describe('Issuable Time Tracker', () => {
});
});
});
+
+ describe('Event listeners', () => {
+ it('refetches issuableTimeTracking query when eventHub emits `timeTracker:refresh` event', async () => {
+ SidebarEventHub.$emit('timeTracker:refresh');
+
+ await wrapper.vm.$nextTick();
+
+ expect(issuableTimeTrackingRefetchSpy).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index b052038661a..d6287b93fb9 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -513,4 +513,100 @@ export const participantsQueryResponse = {
},
};
+export const mockGroupPath = 'gitlab-org';
+export const mockProjectPath = `${mockGroupPath}/some-project`;
+
+export const mockIssue = {
+ projectPath: mockProjectPath,
+ iid: '1',
+ groupPath: mockGroupPath,
+};
+
+export const mockIssueId = 'gid://gitlab/Issue/1';
+
+export const mockMilestone1 = {
+ __typename: 'Milestone',
+ id: 'gid://gitlab/Milestone/1',
+ title: 'Foobar Milestone',
+ webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/1',
+ state: 'active',
+};
+
+export const mockMilestone2 = {
+ __typename: 'Milestone',
+ id: 'gid://gitlab/Milestone/2',
+ title: 'Awesome Milestone',
+ webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/2',
+ state: 'active',
+};
+
+export const mockProjectMilestonesResponse = {
+ data: {
+ workspace: {
+ attributes: {
+ nodes: [mockMilestone1, mockMilestone2],
+ },
+ __typename: 'MilestoneConnection',
+ },
+ __typename: 'Project',
+ },
+};
+
+export const noCurrentMilestoneResponse = {
+ data: {
+ workspace: {
+ issuable: { id: mockIssueId, attribute: null, __typename: 'Issue' },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockMilestoneMutationResponse = {
+ data: {
+ issuableSetAttribute: {
+ errors: [],
+ issuable: {
+ id: 'gid://gitlab/Issue/1',
+ attribute: {
+ id: 'gid://gitlab/Milestone/2',
+ title: 'Awesome Milestone',
+ state: 'active',
+ __typename: 'Milestone',
+ },
+ __typename: 'Issue',
+ },
+ __typename: 'UpdateIssuePayload',
+ },
+ },
+};
+
+export const emptyProjectMilestonesResponse = {
+ data: {
+ workspace: {
+ attributes: {
+ nodes: [],
+ },
+ __typename: 'MilestoneConnection',
+ },
+ __typename: 'Project',
+ },
+};
+
+export const issuableTimeTrackingResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ issuable: {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/1',
+ title: 'Commodi incidunt eos eos libero dicta dolores sed.',
+ timeEstimate: 10_000, // 2h 46m
+ totalTimeSpent: 5_000, // 1h 23m
+ humanTimeEstimate: '2h 46m',
+ humanTotalTimeSpent: '1h 23m',
+ },
+ },
+ },
+};
+
export default mockData;
diff --git a/spec/frontend/sidebar/track_invite_members_spec.js b/spec/frontend/sidebar/track_invite_members_spec.js
new file mode 100644
index 00000000000..6c96e4cfc76
--- /dev/null
+++ b/spec/frontend/sidebar/track_invite_members_spec.js
@@ -0,0 +1,37 @@
+import $ from 'jquery';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import trackShowInviteMemberLink from '~/sidebar/track_invite_members';
+
+describe('Track user dropdown open', () => {
+ let trackingSpy;
+ let dropdownElement;
+
+ beforeEach(() => {
+ document.body.innerHTML = `
+ <div id="dummy-wrapper-element">
+ <div class="js-sidebar-assignee-dropdown">
+ <div class="js-invite-members-track" data-track-event="_track_event_" data-track-label="_track_label_">
+ </div>
+ </div>
+ </div>
+ `;
+
+ dropdownElement = document.querySelector('.js-sidebar-assignee-dropdown');
+ trackingSpy = mockTracking('_category_', dropdownElement, jest.spyOn);
+ document.body.dataset.page = 'some:page';
+
+ trackShowInviteMemberLink(dropdownElement);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('sends a tracking event when the dropdown is opened and contains Invite Members link', () => {
+ $(dropdownElement).trigger('shown.bs.dropdown');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, '_track_event_', {
+ label: '_track_label_',
+ });
+ });
+});