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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-01-18 22:00:14 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-01-18 22:00:14 +0300
commit05f0ebba3a2c8ddf39e436f412dc2ab5bf1353b2 (patch)
tree11d0f2a6ec31c7793c184106cedc2ded3d9a2cc5 /spec/frontend/issues
parentec73467c23693d0db63a797d10194da9e72a74af (diff)
Add latest changes from gitlab-org/gitlab@15-8-stable-eev15.8.0-rc42
Diffstat (limited to 'spec/frontend/issues')
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js173
-rw-r--r--spec/frontend/issues/dashboard/utils_spec.js88
-rw-r--r--spec/frontend/issues/list/mock_data.js19
-rw-r--r--spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js6
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js33
-rw-r--r--spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js9
-rw-r--r--spec/frontend/issues/show/components/incidents/mock_data.js1
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js142
8 files changed, 384 insertions, 87 deletions
diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
index 3f40772f7fc..841cea28ffc 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -27,6 +27,9 @@ import { scrollUp } from '~/lib/utils/scroll_utils';
import {
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableStates } from '~/vue_shared/issuable/list/constants';
@@ -42,8 +45,12 @@ describe('IssuesDashboardApp component', () => {
Vue.use(VueApollo);
const defaultProvide = {
+ autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
calendarPath: 'calendar/path',
- emptyStateSvgPath: 'empty-state.svg',
+ dashboardLabelsPath: 'dashboard/labels/path',
+ dashboardMilestonesPath: 'dashboard/milestones/path',
+ emptyStateWithFilterSvgPath: 'empty/state/with/filter/svg/path.svg',
+ emptyStateWithoutFilterSvgPath: 'empty/state/with/filter/svg/path.svg',
hasBlockedIssuesFeature: true,
hasIssuableHealthStatusFeature: true,
hasIssueWeightsFeature: true,
@@ -97,74 +104,122 @@ describe('IssuesDashboardApp component', () => {
axiosMock.reset();
});
- it('renders IssuableList component', async () => {
- mountComponent();
- jest.runOnlyPendingTimers();
- await waitForPromises();
-
- expect(findIssuableList().props()).toMatchObject({
- currentTab: IssuableStates.Opened,
- hasNextPage: true,
- hasPreviousPage: false,
- hasScopedLabelsFeature: defaultProvide.hasScopedLabelsFeature,
- initialSortBy: CREATED_DESC,
- issuables: issuesQueryResponse.data.issues.nodes,
- issuablesLoading: false,
- namespace: 'dashboard',
- recentSearchesStorageKey: 'issues',
- searchInputPlaceholder: IssuesDashboardApp.i18n.searchInputPlaceholder,
- showPaginationControls: true,
- sortOptions: getSortOptions({
- hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
- hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
- hasIssueWeightsFeature: defaultProvide.hasIssueWeightsFeature,
- }),
- tabs: IssuesDashboardApp.IssuableListTabs,
- urlParams: {
- sort: urlSortParams[CREATED_DESC],
- state: IssuableStates.Opened,
- },
- useKeysetPagination: true,
+ describe('UI components', () => {
+ beforeEach(() => {
+ setWindowLocation(locationSearch);
+ mountComponent();
+ jest.runOnlyPendingTimers();
+ return waitForPromises();
});
- });
- it('renders RSS button link', () => {
- mountComponent();
+ it('renders IssuableList component', () => {
+ expect(findIssuableList().props()).toMatchObject({
+ currentTab: IssuableStates.Opened,
+ hasNextPage: true,
+ hasPreviousPage: false,
+ hasScopedLabelsFeature: defaultProvide.hasScopedLabelsFeature,
+ initialSortBy: CREATED_DESC,
+ issuables: issuesQueryResponse.data.issues.nodes,
+ issuablesLoading: false,
+ namespace: 'dashboard',
+ recentSearchesStorageKey: 'issues',
+ searchInputPlaceholder: IssuesDashboardApp.i18n.searchInputPlaceholder,
+ showPaginationControls: true,
+ sortOptions: getSortOptions({
+ hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: defaultProvide.hasIssueWeightsFeature,
+ }),
+ tabs: IssuesDashboardApp.IssuableListTabs,
+ urlParams: {
+ sort: urlSortParams[CREATED_DESC],
+ state: IssuableStates.Opened,
+ },
+ useKeysetPagination: true,
+ });
+ });
- expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath);
- expect(findRssButton().props('icon')).toBe('rss');
- });
+ it('renders RSS button link', () => {
+ expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath);
+ });
- it('renders calendar button link', () => {
- mountComponent();
+ it('renders calendar button link', () => {
+ expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath);
+ });
+
+ it('renders issue time information', () => {
+ expect(findIssueCardTimeInfo().exists()).toBe(true);
+ });
- expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath);
- expect(findCalendarButton().props('icon')).toBe('calendar');
+ it('renders issue statistics', () => {
+ expect(findIssueCardStatistics().exists()).toBe(true);
+ });
});
- it('renders issue time information', async () => {
- mountComponent();
- jest.runOnlyPendingTimers();
- await waitForPromises();
+ describe('fetching issues', () => {
+ describe('with a search query', () => {
+ describe('when there are issues returned', () => {
+ beforeEach(() => {
+ setWindowLocation(locationSearch);
+ mountComponent();
+ jest.runOnlyPendingTimers();
+ return waitForPromises();
+ });
- expect(findIssueCardTimeInfo().exists()).toBe(true);
- });
+ it('renders the issues', () => {
+ expect(findIssuableList().props('issuables')).toEqual(
+ defaultQueryResponse.data.issues.nodes,
+ );
+ });
- it('renders issue statistics', async () => {
- mountComponent();
- jest.runOnlyPendingTimers();
- await waitForPromises();
+ it('does not render empty state', () => {
+ expect(findEmptyState().exists()).toBe(false);
+ });
+ });
- expect(findIssueCardStatistics().exists()).toBe(true);
- });
+ describe('when there are no issues returned', () => {
+ beforeEach(() => {
+ setWindowLocation(locationSearch);
+ mountComponent({
+ issuesQueryHandler: jest.fn().mockResolvedValue(emptyIssuesQueryResponse),
+ });
+ return waitForPromises();
+ });
+
+ it('renders no issues', () => {
+ expect(findIssuableList().props('issuables')).toEqual([]);
+ });
+
+ it('renders empty state', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ description: IssuesDashboardApp.i18n.emptyStateWithFilterDescription,
+ svgPath: defaultProvide.emptyStateWithFilterSvgPath,
+ title: IssuesDashboardApp.i18n.emptyStateWithFilterTitle,
+ });
+ });
+ });
+ });
+
+ describe('with no search query', () => {
+ let issuesQueryHandler;
+
+ beforeEach(() => {
+ issuesQueryHandler = jest.fn().mockResolvedValue(defaultQueryResponse);
+ mountComponent({ issuesQueryHandler });
+ return waitForPromises();
+ });
- it('renders empty state', async () => {
- mountComponent({ issuesQueryHandler: jest.fn().mockResolvedValue(emptyIssuesQueryResponse) });
- await waitForPromises();
+ it('does not call issues query', () => {
+ expect(issuesQueryHandler).not.toHaveBeenCalled();
+ });
- expect(findEmptyState().props()).toMatchObject({
- svgPath: defaultProvide.emptyStateSvgPath,
- title: IssuesDashboardApp.i18n.emptyStateTitle,
+ it('renders empty state', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ description: null,
+ svgPath: defaultProvide.emptyStateWithoutFilterSvgPath,
+ title: IssuesDashboardApp.i18n.emptyStateWithoutFilterTitle,
+ });
+ });
});
});
@@ -233,6 +288,7 @@ describe('IssuesDashboardApp component', () => {
describe('when there is an error fetching issues', () => {
beforeEach(() => {
+ setWindowLocation(locationSearch);
mountComponent({ issuesQueryHandler: jest.fn().mockRejectedValue(new Error('ERROR')) });
jest.runOnlyPendingTimers();
return waitForPromises();
@@ -281,6 +337,9 @@ describe('IssuesDashboardApp component', () => {
expect(findIssuableList().props('searchTokens')).toMatchObject([
{ type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers },
+ { type: TOKEN_TYPE_LABEL },
+ { type: TOKEN_TYPE_MILESTONE },
+ { type: TOKEN_TYPE_MY_REACTION },
]);
});
});
diff --git a/spec/frontend/issues/dashboard/utils_spec.js b/spec/frontend/issues/dashboard/utils_spec.js
new file mode 100644
index 00000000000..08d00eee3e3
--- /dev/null
+++ b/spec/frontend/issues/dashboard/utils_spec.js
@@ -0,0 +1,88 @@
+import AxiosMockAdapter from 'axios-mock-adapter';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { AutocompleteCache } from '~/issues/dashboard/utils';
+import { MAX_LIST_SIZE } from '~/issues/list/constants';
+import axios from '~/lib/utils/axios_utils';
+
+describe('AutocompleteCache', () => {
+ let autocompleteCache;
+ let axiosMock;
+ const cacheName = 'name';
+ const searchProperty = 'property';
+ const url = 'url';
+
+ const data = [
+ { [searchProperty]: 'one' },
+ { [searchProperty]: 'two' },
+ { [searchProperty]: 'three' },
+ { [searchProperty]: 'four' },
+ { [searchProperty]: 'five' },
+ { [searchProperty]: 'six' },
+ { [searchProperty]: 'seven' },
+ { [searchProperty]: 'eight' },
+ { [searchProperty]: 'nine' },
+ { [searchProperty]: 'ten' },
+ { [searchProperty]: 'eleven' },
+ { [searchProperty]: 'twelve' },
+ { [searchProperty]: 'thirteen' },
+ { [searchProperty]: 'fourteen' },
+ { [searchProperty]: 'fifteen' },
+ ];
+
+ beforeEach(() => {
+ autocompleteCache = new AutocompleteCache();
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.reset();
+ });
+
+ describe('when there is no cached data', () => {
+ let response;
+
+ beforeEach(async () => {
+ axiosMock.onGet(url).replyOnce(200, data);
+ response = await autocompleteCache.fetch({ url, cacheName, searchProperty });
+ });
+
+ it('fetches items via the API', () => {
+ expect(axiosMock.history.get[0].url).toBe(url);
+ });
+
+ it('returns a maximum of 10 items', () => {
+ expect(response).toHaveLength(MAX_LIST_SIZE);
+ });
+ });
+
+ describe('when there is cached data', () => {
+ let response;
+
+ beforeEach(async () => {
+ axiosMock.onGet(url).replyOnce(200, data);
+ jest.spyOn(fuzzaldrinPlus, 'filter');
+ // Populate cache
+ await autocompleteCache.fetch({ url, cacheName, searchProperty });
+ // Execute filtering on cache data
+ response = await autocompleteCache.fetch({ url, cacheName, searchProperty, search: 'een' });
+ });
+
+ it('returns filtered items based on search characters', () => {
+ expect(response).toEqual([
+ { [searchProperty]: 'fifteen' },
+ { [searchProperty]: 'thirteen' },
+ { [searchProperty]: 'fourteen' },
+ { [searchProperty]: 'eleven' },
+ { [searchProperty]: 'seven' },
+ ]);
+ });
+
+ it('filters using fuzzaldrinPlus', () => {
+ expect(fuzzaldrinPlus.filter).toHaveBeenCalled();
+ });
+
+ it('does not call the API', () => {
+ expect(axiosMock.history.get[1]).toBeUndefined();
+ });
+ });
+});
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 0690501dee9..70b1521ff70 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -16,6 +16,7 @@ import {
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
TOKEN_TYPE_WEIGHT,
+ TOKEN_TYPE_HEALTH,
} from '~/vue_shared/components/filtered_search_bar/constants';
export const getIssuesQueryResponse = {
@@ -149,6 +150,8 @@ export const locationSearch = [
'label_name[]=tv',
'not[label_name][]=live action',
'not[label_name][]=drama',
+ 'or[label_name][]=comedy',
+ 'or[label_name][]=sitcom',
'release_tag=v3',
'release_tag=v4',
'not[release_tag]=v20',
@@ -170,6 +173,8 @@ export const locationSearch = [
'not[weight]=3',
'crm_contact_id=123',
'crm_organization_id=456',
+ 'health_status=atRisk',
+ 'not[health_status]=onTrack',
].join('&');
export const locationSearchWithSpecialValues = [
@@ -182,6 +187,7 @@ export const locationSearchWithSpecialValues = [
'milestone_title=Upcoming',
'epic_id=None',
'weight=None',
+ 'health_status=None',
].join('&');
export const filteredTokens = [
@@ -204,6 +210,8 @@ export const filteredTokens = [
{ type: TOKEN_TYPE_LABEL, value: { data: 'tv', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'live action', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'drama', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'comedy', operator: OPERATOR_OR } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'sitcom', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v3', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v4', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v20', operator: OPERATOR_NOT } },
@@ -225,6 +233,8 @@ export const filteredTokens = [
{ type: TOKEN_TYPE_WEIGHT, value: { data: '3', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_CONTACT, value: { data: '123', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ORGANIZATION, value: { data: '456', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_HEALTH, value: { data: 'atRisk', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_HEALTH, value: { data: 'onTrack', operator: OPERATOR_NOT } },
{ type: FILTERED_SEARCH_TERM, value: { data: 'find' } },
{ type: FILTERED_SEARCH_TERM, value: { data: 'issues' } },
];
@@ -239,6 +249,7 @@ export const filteredTokensWithSpecialValues = [
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'Upcoming', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_EPIC, value: { data: 'None', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_WEIGHT, value: { data: 'None', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_HEALTH, value: { data: 'None', operator: OPERATOR_IS } },
];
export const apiParams = {
@@ -255,6 +266,7 @@ export const apiParams = {
weight: '1',
crmContactId: '123',
crmOrganizationId: '456',
+ healthStatusFilter: 'atRisk',
not: {
authorUsername: 'marge',
assigneeUsernames: ['patty', 'selma'],
@@ -266,10 +278,12 @@ export const apiParams = {
iterationId: ['20', '42'],
epicId: '34',
weight: '3',
+ healthStatusFilter: 'onTrack',
},
or: {
authorUsernames: ['burns', 'smithers'],
assigneeUsernames: ['carl', 'lenny'],
+ labelNames: ['comedy', 'sitcom'],
},
};
@@ -283,6 +297,7 @@ export const apiParamsWithSpecialValues = {
milestoneWildcardId: 'UPCOMING',
epicId: 'None',
weight: 'None',
+ healthStatusFilter: 'NONE',
};
export const urlParams = {
@@ -296,6 +311,7 @@ export const urlParams = {
'not[milestone_title]': ['season 20', 'season 30'],
'label_name[]': ['cartoon', 'tv'],
'not[label_name][]': ['live action', 'drama'],
+ 'or[label_name][]': ['comedy', 'sitcom'],
release_tag: ['v3', 'v4'],
'not[release_tag]': ['v20', 'v30'],
'type[]': ['issue', 'feature'],
@@ -311,6 +327,8 @@ export const urlParams = {
'not[weight]': '3',
crm_contact_id: '123',
crm_organization_id: '456',
+ health_status: 'atRisk',
+ 'not[health_status]': 'onTrack',
};
export const urlParamsWithSpecialValues = {
@@ -323,6 +341,7 @@ export const urlParamsWithSpecialValues = {
milestone_title: 'Upcoming',
epic_id: 'None',
weight: 'None',
+ health_status: 'None',
};
export const project1 = {
diff --git a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
index d30a8c081cc..8413b8463c1 100644
--- a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
+++ b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
@@ -1,4 +1,4 @@
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import mockData from 'test_fixtures/issues/related_merge_requests.json';
import axios from '~/lib/utils/axios_utils';
@@ -20,7 +20,7 @@ describe('RelatedMergeRequests', () => {
mock = new MockAdapter(axios);
mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(200, mockData, { 'x-total': 2 });
- wrapper = mount(RelatedMergeRequests, {
+ wrapper = shallowMount(RelatedMergeRequests, {
store: createStore(),
propsData: {
endpoint: API_ENDPOINT,
@@ -49,7 +49,7 @@ describe('RelatedMergeRequests', () => {
});
});
- it('should return an array with single assingee', () => {
+ it('should return an array with single assignee', () => {
const mr = { assignee: assignees[0] };
expect(wrapper.vm.getAssignees(mr)).toEqual([assignees[0]]);
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index 7d6ca44e679..aaf228ae181 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -6,6 +6,7 @@ import { mockTracking } from 'helpers/tracking_helper';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { IssuableStatus, IssueType } from '~/issues/constants';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import HeaderActions from '~/issues/show/components/header_actions.vue';
import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutation.graphql';
@@ -38,8 +39,9 @@ describe('HeaderActions component', () => {
issueType: IssueType.Issue,
newIssuePath: 'gitlab-org/gitlab-test/-/issues/new',
projectPath: 'gitlab-org/gitlab-test',
- reportAbusePath:
- '-/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%2Fgitlab-org%2Fgitlab-test%2F-%2Fissues%2F32&user_id=1',
+ reportAbusePath: '-/abuse_reports/add_category',
+ reportedUserId: '1',
+ reportedFromUrl: 'http://localhost:/gitlab-org/-/issues/32',
submitAsSpamPath: 'gitlab-org/gitlab-test/-/issues/32/submit_as_spam',
};
@@ -401,4 +403,31 @@ describe('HeaderActions component', () => {
});
});
});
+
+ describe('abuse category selector', () => {
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
+
+ beforeEach(() => {
+ wrapper = mountComponent({ props: { isIssueAuthor: false } });
+ });
+
+ it('renders', () => {
+ expect(findAbuseCategorySelector().exists()).toBe(true);
+ expect(findAbuseCategorySelector().props('showDrawer')).toEqual(false);
+ });
+
+ it('opens the drawer', async () => {
+ findDesktopDropdownItems().at(2).vm.$emit('click');
+
+ await nextTick();
+
+ expect(findAbuseCategorySelector().props('showDrawer')).toEqual(true);
+ });
+
+ it('closes the drawer', async () => {
+ await findAbuseCategorySelector().vm.$emit('close-drawer');
+
+ expect(findAbuseCategorySelector().props('showDrawer')).toEqual(false);
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
index 1286617d64a..6c923cae0cc 100644
--- a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
@@ -1,6 +1,6 @@
import VueApollo from 'vue-apollo';
import Vue from 'vue';
-import { GlDatepicker } from '@gitlab/ui';
+import { GlDatepicker, GlListboxItem } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue';
@@ -27,6 +27,7 @@ const mockInputData = {
incidentId: 'gid://gitlab/Issue/1',
note: 'test',
occurredAt: '2020-07-08T00:00:00.000Z',
+ timelineEventTagNames: ['Start time'],
};
describe('Create Timeline events', () => {
@@ -51,9 +52,14 @@ describe('Create Timeline events', () => {
findHourInput().setValue(inputDate.getHours());
findMinuteInput().setValue(inputDate.getMinutes());
};
+ const findListboxItems = () => wrapper.findAllComponents(GlListboxItem);
+ const setEventTags = () => {
+ findListboxItems().at(0).vm.$emit('select', true);
+ };
const fillForm = () => {
setDatetime();
setNoteInput();
+ setEventTags();
};
function createMockApolloProvider() {
@@ -80,6 +86,7 @@ describe('Create Timeline events', () => {
provide: {
fullPath: 'group/project',
issuableId: '1',
+ glFeatures: { incidentEventTags: true },
},
apolloProvider,
});
diff --git a/spec/frontend/issues/show/components/incidents/mock_data.js b/spec/frontend/issues/show/components/incidents/mock_data.js
index 9accfcea791..6606bed1567 100644
--- a/spec/frontend/issues/show/components/incidents/mock_data.js
+++ b/spec/frontend/issues/show/components/incidents/mock_data.js
@@ -74,6 +74,7 @@ const mockUpdatedEvent = {
action: 'comment',
occurredAt: '2022-07-01T12:47:00Z',
createdAt: '2022-07-20T12:47:40Z',
+ timelineEventTags: [],
};
export const timelineEventsQueryListResponse = {
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
index d5b199cc790..f06d968a4c5 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
@@ -1,11 +1,15 @@
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
-import { GlDatepicker } from '@gitlab/ui';
-import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import { GlDatepicker, GlListbox } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { timelineFormI18n } from '~/issues/show/components/incidents/constants';
+import {
+ timelineFormI18n,
+ TIMELINE_EVENT_TAGS,
+ timelineEventTagsI18n,
+} from '~/issues/show/components/incidents/constants';
import { createAlert } from '~/flash';
import { useFakeDate } from 'helpers/fake_date';
@@ -17,17 +21,23 @@ const fakeDate = '2020-07-08T00:00:00.000Z';
const mockInputDate = new Date('2021-08-12');
+const mockTags = TIMELINE_EVENT_TAGS;
+
describe('Timeline events form', () => {
// July 8 2020
useFakeDate(fakeDate);
let wrapper;
- const mountComponent = ({ mountMethod = shallowMountExtended } = {}, props = {}) => {
+ const mountComponent = ({ mountMethod = mountExtended } = {}, props = {}, glFeatures = {}) => {
wrapper = mountMethod(TimelineEventsForm, {
+ provide: {
+ glFeatures,
+ },
propsData: {
showSaveAndAdd: true,
isEventProcessed: false,
...props,
+ tags: mockTags,
},
stubs: {
GlButton: true,
@@ -35,6 +45,10 @@ describe('Timeline events form', () => {
});
};
+ beforeEach(() => {
+ mountComponent();
+ });
+
afterEach(() => {
createAlert.mockReset();
wrapper.destroy();
@@ -48,16 +62,26 @@ describe('Timeline events form', () => {
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
const findHourInput = () => wrapper.findByTestId('input-hours');
const findMinuteInput = () => wrapper.findByTestId('input-minutes');
- const setDatetime = () => {
- findDatePicker().vm.$emit('input', mockInputDate);
- findHourInput().setValue(5);
- findMinuteInput().setValue(45);
- };
+ const findTagDropdown = () => wrapper.findComponent(GlListbox);
const findTextarea = () => wrapper.findByTestId('input-note');
+ const findTextareaValue = () => findTextarea().element.value;
const findCountNumeric = (count) => wrapper.findByText(count);
const findCountVerbose = (count) => wrapper.findByText(`${count} characters remaining`);
const findCountHint = () => wrapper.findByText(timelineFormI18n.hint);
+ const setDatetime = () => {
+ findDatePicker().vm.$emit('input', mockInputDate);
+ findHourInput().setValue(5);
+ findMinuteInput().setValue(45);
+ };
+ const selectTags = async (tags) => {
+ findTagDropdown().vm.$emit(
+ 'select',
+ tags.map((x) => x.value),
+ );
+ await nextTick();
+ };
+ const selectOneTag = () => selectTags([mockTags[0]]);
const submitForm = async () => {
findSubmitButton().vm.$emit('click');
await waitForPromises();
@@ -90,23 +114,97 @@ describe('Timeline events form', () => {
]);
});
- describe('form button behaviour', () => {
+ describe('with incident_event_tag feature flag enabled', () => {
beforeEach(() => {
- mountComponent({ mountMethod: mountExtended });
+ mountComponent(
+ {},
+ {},
+ {
+ incidentEventTags: true,
+ },
+ );
+ });
+
+ describe('event tag dropdown', () => {
+ it('should render option list from provided array', () => {
+ expect(findTagDropdown().props('items')).toEqual(mockTags);
+ });
+
+ it('should allow to choose multiple tags', async () => {
+ await selectTags(mockTags);
+
+ expect(findTagDropdown().props('selected')).toEqual(mockTags.map((x) => x.value));
+ });
+
+ it('should show default option, when none is chosen', () => {
+ expect(findTagDropdown().props('toggleText')).toBe(timelineFormI18n.selectTags);
+ });
+
+ it('should show the tag, when one is selected', async () => {
+ await selectOneTag();
+
+ expect(findTagDropdown().props('toggleText')).toBe(timelineEventTagsI18n.startTime);
+ });
+
+ it('should show the number of selected tags, when more than one is selected', async () => {
+ await selectTags(mockTags);
+
+ expect(findTagDropdown().props('toggleText')).toBe('2 tags');
+ });
+
+ it('should be cleared when clear is triggered', async () => {
+ await selectTags(mockTags);
+
+ // This component expects the parent to call `clear`, so this is the only way to trigger this
+ wrapper.vm.clear();
+ await nextTick();
+
+ expect(findTagDropdown().props('toggleText')).toBe(timelineFormI18n.selectTags);
+ expect(findTagDropdown().props('selected')).toEqual([]);
+ });
+
+ it('should populate incident note with tags if a note was empty', async () => {
+ await selectTags(mockTags);
+
+ expect(findTextareaValue()).toBe(
+ `${timelineFormI18n.areaDefaultMessage} ${mockTags
+ .map((x) => x.value.toLowerCase())
+ .join(', ')}`,
+ );
+ });
+
+ it('should populate incident note with tag but allow to customise it', async () => {
+ await selectOneTag();
+
+ await findTextarea().setValue('my customised event note');
+
+ await nextTick();
+
+ expect(findTextareaValue()).toBe('my customised event note');
+ });
+
+ it('should not populate incident note with tag if it had a note', async () => {
+ await findTextarea().setValue('hello');
+ await selectOneTag();
+
+ expect(findTextareaValue()).toBe('hello');
+ });
});
+ });
+ describe('form button behaviour', () => {
it('should save event on submit', async () => {
await submitForm();
expect(wrapper.emitted()).toEqual({
- 'save-event': [[{ note: '', occurredAt: fakeDate }, false]],
+ 'save-event': [[{ note: '', occurredAt: fakeDate, timelineEventTags: [] }, false]],
});
});
it('should save event on "submit and add another"', async () => {
await submitFormAndAddAnother();
expect(wrapper.emitted()).toEqual({
- 'save-event': [[{ note: '', occurredAt: fakeDate }, true]],
+ 'save-event': [[{ note: '', occurredAt: fakeDate, timelineEventTags: [] }, true]],
});
});
@@ -145,10 +243,6 @@ describe('Timeline events form', () => {
});
describe('form character limit', () => {
- beforeEach(() => {
- mountComponent({ mountMethod: mountExtended });
- });
-
it('sets a character limit hint', () => {
expect(findCountHint().exists()).toBe(true);
});
@@ -172,32 +266,32 @@ describe('Timeline events form', () => {
});
describe('Delete button', () => {
- it('does not show the delete button if showDelete prop is false', () => {
- mountComponent({ mountMethod: mountExtended }, { showDelete: false });
+ it('does not show the delete button if isEditing prop is false', () => {
+ mountComponent({ mountMethod: mountExtended }, { isEditing: false });
expect(findDeleteButton().exists()).toBe(false);
});
- it('shows the delete button if showDelete prop is true', () => {
- mountComponent({ mountMethod: mountExtended }, { showDelete: true });
+ it('shows the delete button if isEditing prop is true', () => {
+ mountComponent({ mountMethod: mountExtended }, { isEditing: true });
expect(findDeleteButton().exists()).toBe(true);
});
it('disables the delete button if isEventProcessed prop is true', () => {
- mountComponent({ mountMethod: mountExtended }, { showDelete: true, isEventProcessed: true });
+ mountComponent({ mountMethod: mountExtended }, { isEditing: true, isEventProcessed: true });
expect(findDeleteButton().props('disabled')).toBe(true);
});
it('does not disable the delete button if isEventProcessed prop is false', () => {
- mountComponent({ mountMethod: mountExtended }, { showDelete: true, isEventProcessed: false });
+ mountComponent({ mountMethod: mountExtended }, { isEditing: true, isEventProcessed: false });
expect(findDeleteButton().props('disabled')).toBe(false);
});
it('emits delete event on click', () => {
- mountComponent({ mountMethod: mountExtended }, { showDelete: true, isEventProcessed: true });
+ mountComponent({ mountMethod: mountExtended }, { isEditing: true, isEventProcessed: true });
deleteForm();