diff options
Diffstat (limited to 'spec/frontend/issues')
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(); |