diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-20 09:16:11 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-20 09:16:11 +0000 |
commit | edaa33dee2ff2f7ea3fac488d41558eb5f86d68c (patch) | |
tree | 11f143effbfeba52329fb7afbd05e6e2a3790241 /spec/frontend/issues/list/components/issues_list_app_spec.js | |
parent | d8a5691316400a0f7ec4f83832698f1988eb27c1 (diff) | |
download | gitlab-ce-edaa33dee2ff2f7ea3fac488d41558eb5f86d68c.tar.gz |
Add latest changes from gitlab-org/gitlab@14-7-stable-eev14.7.0-rc42
Diffstat (limited to 'spec/frontend/issues/list/components/issues_list_app_spec.js')
-rw-r--r-- | spec/frontend/issues/list/components/issues_list_app_spec.js | 829 |
1 files changed, 829 insertions, 0 deletions
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js new file mode 100644 index 00000000000..66428ee0492 --- /dev/null +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -0,0 +1,829 @@ +import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; +import { mount, shallowMount } from '@vue/test-utils'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import { cloneDeep } from 'lodash'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql'; +import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import setWindowLocation from 'helpers/set_window_location_helper'; +import { TEST_HOST } from 'helpers/test_constants'; +import waitForPromises from 'helpers/wait_for_promises'; +import { + getIssuesCountsQueryResponse, + getIssuesQueryResponse, + filteredTokens, + locationSearch, + urlParams, +} from 'jest/issues/list/mock_data'; +import createFlash, { FLASH_TYPES } from '~/flash'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; +import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; +import IssuableByEmail from '~/issuable/components/issuable_by_email.vue'; +import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; +import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants'; +import IssuesListApp from '~/issues/list/components/issues_list_app.vue'; +import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue'; +import { + CREATED_DESC, + DUE_DATE_OVERDUE, + PARAM_DUE_DATE, + RELATIVE_POSITION, + RELATIVE_POSITION_ASC, + TOKEN_TYPE_ASSIGNEE, + TOKEN_TYPE_AUTHOR, + TOKEN_TYPE_CONFIDENTIAL, + TOKEN_TYPE_LABEL, + TOKEN_TYPE_MILESTONE, + TOKEN_TYPE_MY_REACTION, + TOKEN_TYPE_RELEASE, + TOKEN_TYPE_TYPE, + urlSortParams, +} from '~/issues/list/constants'; +import eventHub from '~/issues/list/eventhub'; +import { getSortOptions } from '~/issues/list/utils'; +import axios from '~/lib/utils/axios_utils'; +import { scrollUp } from '~/lib/utils/scroll_utils'; +import { joinPaths } from '~/lib/utils/url_utility'; + +jest.mock('@sentry/browser'); +jest.mock('~/flash'); +jest.mock('~/lib/utils/scroll_utils', () => ({ + scrollUp: jest.fn().mockName('scrollUpMock'), +})); + +describe('CE IssuesListApp component', () => { + let axiosMock; + let wrapper; + + Vue.use(VueApollo); + + const defaultProvide = { + calendarPath: 'calendar/path', + canBulkUpdate: false, + emptyStateSvgPath: 'empty-state.svg', + exportCsvPath: 'export/csv/path', + fullPath: 'path/to/project', + hasAnyIssues: true, + hasAnyProjects: true, + hasBlockedIssuesFeature: true, + hasIssuableHealthStatusFeature: true, + hasIssueWeightsFeature: true, + hasIterationsFeature: true, + isProject: true, + isSignedIn: true, + jiraIntegrationPath: 'jira/integration/path', + newIssuePath: 'new/issue/path', + rssPath: 'rss/path', + showNewIssueLink: true, + signInPath: 'sign/in/path', + }; + + let defaultQueryResponse = getIssuesQueryResponse; + if (IS_EE) { + defaultQueryResponse = cloneDeep(getIssuesQueryResponse); + defaultQueryResponse.data.project.issues.nodes[0].blockingCount = 1; + defaultQueryResponse.data.project.issues.nodes[0].healthStatus = null; + defaultQueryResponse.data.project.issues.nodes[0].weight = 5; + } + + const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons); + const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail); + const findGlButton = () => wrapper.findComponent(GlButton); + const findGlButtons = () => wrapper.findAllComponents(GlButton); + const findGlButtonAt = (index) => findGlButtons().at(index); + const findGlEmptyState = () => wrapper.findComponent(GlEmptyState); + const findGlLink = () => wrapper.findComponent(GlLink); + const findIssuableList = () => wrapper.findComponent(IssuableList); + const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown); + + const mountComponent = ({ + provide = {}, + issuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse), + issuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse), + mountFn = shallowMount, + } = {}) => { + const requestHandlers = [ + [getIssuesQuery, issuesQueryResponse], + [getIssuesCountsQuery, issuesCountsQueryResponse], + ]; + const apolloProvider = createMockApollo(requestHandlers); + + return mountFn(IssuesListApp, { + apolloProvider, + provide: { + ...defaultProvide, + ...provide, + }, + }); + }; + + beforeEach(() => { + setWindowLocation(TEST_HOST); + axiosMock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + axiosMock.reset(); + wrapper.destroy(); + }); + + describe('IssuableList', () => { + beforeEach(() => { + wrapper = mountComponent(); + jest.runOnlyPendingTimers(); + }); + + it('renders', () => { + expect(findIssuableList().props()).toMatchObject({ + namespace: defaultProvide.fullPath, + recentSearchesStorageKey: 'issues', + searchInputPlaceholder: IssuesListApp.i18n.searchPlaceholder, + sortOptions: getSortOptions(true, true), + initialSortBy: CREATED_DESC, + issuables: getIssuesQueryResponse.data.project.issues.nodes, + tabs: IssuableListTabs, + currentTab: IssuableStates.Opened, + tabCounts: { + opened: 1, + closed: 1, + all: 1, + }, + issuablesLoading: false, + isManualOrdering: false, + showBulkEditSidebar: false, + showPaginationControls: true, + useKeysetPagination: true, + hasPreviousPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasPreviousPage, + hasNextPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasNextPage, + urlParams: { + sort: urlSortParams[CREATED_DESC], + state: IssuableStates.Opened, + }, + }); + }); + }); + + describe('header action buttons', () => { + it('renders rss button', () => { + wrapper = mountComponent({ mountFn: mount }); + + expect(findGlButtonAt(0).props('icon')).toBe('rss'); + expect(findGlButtonAt(0).attributes()).toMatchObject({ + href: defaultProvide.rssPath, + 'aria-label': IssuesListApp.i18n.rssLabel, + }); + }); + + it('renders calendar button', () => { + wrapper = mountComponent({ mountFn: mount }); + + expect(findGlButtonAt(1).props('icon')).toBe('calendar'); + expect(findGlButtonAt(1).attributes()).toMatchObject({ + href: defaultProvide.calendarPath, + 'aria-label': IssuesListApp.i18n.calendarLabel, + }); + }); + + describe('csv import/export component', () => { + describe('when user is signed in', () => { + const search = '?search=refactor&sort=created_date&state=opened'; + + beforeEach(() => { + setWindowLocation(search); + + wrapper = mountComponent({ provide: { isSignedIn: true }, mountFn: mount }); + + jest.runOnlyPendingTimers(); + }); + + it('renders', () => { + expect(findCsvImportExportButtons().props()).toMatchObject({ + exportCsvPath: `${defaultProvide.exportCsvPath}${search}`, + issuableCount: 1, + }); + }); + }); + + describe('when user is not signed in', () => { + it('does not render', () => { + wrapper = mountComponent({ provide: { isSignedIn: false }, mountFn: mount }); + + expect(findCsvImportExportButtons().exists()).toBe(false); + }); + }); + + describe('when in a group context', () => { + it('does not render', () => { + wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount }); + + expect(findCsvImportExportButtons().exists()).toBe(false); + }); + }); + }); + + describe('bulk edit button', () => { + it('renders when user has permissions', () => { + wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount }); + + expect(findGlButtonAt(2).text()).toBe('Edit issues'); + }); + + it('does not render when user does not have permissions', () => { + wrapper = mountComponent({ provide: { canBulkUpdate: false }, mountFn: mount }); + + expect(findGlButtons().filter((button) => button.text() === 'Edit issues')).toHaveLength(0); + }); + + it('emits "issuables:enableBulkEdit" event to legacy bulk edit class', async () => { + wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount }); + + jest.spyOn(eventHub, '$emit'); + + findGlButtonAt(2).vm.$emit('click'); + + await waitForPromises(); + + expect(eventHub.$emit).toHaveBeenCalledWith('issuables:enableBulkEdit'); + }); + }); + + describe('new issue button', () => { + it('renders when user has permissions', () => { + wrapper = mountComponent({ provide: { showNewIssueLink: true }, mountFn: mount }); + + expect(findGlButtonAt(2).text()).toBe('New issue'); + expect(findGlButtonAt(2).attributes('href')).toBe(defaultProvide.newIssuePath); + }); + + it('does not render when user does not have permissions', () => { + wrapper = mountComponent({ provide: { showNewIssueLink: false }, mountFn: mount }); + + expect(findGlButtons().filter((button) => button.text() === 'New issue')).toHaveLength(0); + }); + }); + + describe('new issue split dropdown', () => { + it('does not render in a project context', () => { + wrapper = mountComponent({ provide: { isProject: true }, mountFn: mount }); + + expect(findNewIssueDropdown().exists()).toBe(false); + }); + + it('renders in a group context', () => { + wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount }); + + expect(findNewIssueDropdown().exists()).toBe(true); + }); + }); + }); + + describe('initial url params', () => { + describe('due_date', () => { + it('is set from the url params', () => { + setWindowLocation(`?${PARAM_DUE_DATE}=${DUE_DATE_OVERDUE}`); + + wrapper = mountComponent(); + + expect(findIssuableList().props('urlParams')).toMatchObject({ due_date: DUE_DATE_OVERDUE }); + }); + }); + + describe('search', () => { + it('is set from the url params', () => { + setWindowLocation(locationSearch); + + wrapper = mountComponent(); + + expect(findIssuableList().props('urlParams')).toMatchObject({ search: 'find issues' }); + }); + }); + + describe('sort', () => { + it.each(Object.keys(urlSortParams))('is set as %s from the url params', (sortKey) => { + setWindowLocation(`?sort=${urlSortParams[sortKey]}`); + + wrapper = mountComponent(); + + expect(findIssuableList().props()).toMatchObject({ + initialSortBy: sortKey, + urlParams: { + sort: urlSortParams[sortKey], + }, + }); + }); + + describe('when issue repositioning is disabled and the sort is manual', () => { + beforeEach(() => { + setWindowLocation(`?sort=${RELATIVE_POSITION}`); + wrapper = mountComponent({ provide: { isIssueRepositioningDisabled: true } }); + }); + + it('changes the sort to the default of created descending', () => { + expect(findIssuableList().props()).toMatchObject({ + initialSortBy: CREATED_DESC, + urlParams: { + sort: urlSortParams[CREATED_DESC], + }, + }); + }); + + it('shows an alert to tell the user that manual reordering is disabled', () => { + expect(createFlash).toHaveBeenCalledWith({ + message: IssuesListApp.i18n.issueRepositioningMessage, + type: FLASH_TYPES.NOTICE, + }); + }); + }); + }); + + describe('state', () => { + it('is set from the url params', () => { + const initialState = IssuableStates.All; + + setWindowLocation(`?state=${initialState}`); + + wrapper = mountComponent(); + + expect(findIssuableList().props('currentTab')).toBe(initialState); + }); + }); + + describe('filter tokens', () => { + it('is set from the url params', () => { + setWindowLocation(locationSearch); + + wrapper = mountComponent(); + + expect(findIssuableList().props('initialFilterValue')).toEqual(filteredTokens); + }); + + describe('when anonymous searching is performed', () => { + beforeEach(() => { + setWindowLocation(locationSearch); + + wrapper = mountComponent({ + provide: { isAnonymousSearchDisabled: true, isSignedIn: false }, + }); + }); + + it('is not set from url params', () => { + expect(findIssuableList().props('initialFilterValue')).toEqual([]); + }); + + it('shows an alert to tell the user they must be signed in to search', () => { + expect(createFlash).toHaveBeenCalledWith({ + message: IssuesListApp.i18n.anonymousSearchingMessage, + type: FLASH_TYPES.NOTICE, + }); + }); + }); + }); + }); + + describe('bulk edit', () => { + describe.each([true, false])( + 'when "issuables:toggleBulkEdit" event is received with payload `%s`', + (isBulkEdit) => { + beforeEach(() => { + wrapper = mountComponent(); + + eventHub.$emit('issuables:toggleBulkEdit', isBulkEdit); + }); + + it(`${isBulkEdit ? 'enables' : 'disables'} bulk edit`, () => { + expect(findIssuableList().props('showBulkEditSidebar')).toBe(isBulkEdit); + }); + }, + ); + }); + + describe('IssuableByEmail component', () => { + describe.each([true, false])(`when issue creation by email is enabled=%s`, (enabled) => { + it(`${enabled ? 'renders' : 'does not render'}`, () => { + wrapper = mountComponent({ provide: { initialEmail: enabled } }); + + expect(findIssuableByEmail().exists()).toBe(enabled); + }); + }); + }); + + describe('empty states', () => { + describe('when there are issues', () => { + describe('when search returns no results', () => { + beforeEach(() => { + setWindowLocation(`?search=no+results`); + + wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount }); + }); + + it('shows empty state', () => { + expect(findGlEmptyState().props()).toMatchObject({ + description: IssuesListApp.i18n.noSearchResultsDescription, + title: IssuesListApp.i18n.noSearchResultsTitle, + svgPath: defaultProvide.emptyStateSvgPath, + }); + }); + }); + + describe('when "Open" tab has no issues', () => { + beforeEach(() => { + wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount }); + }); + + it('shows empty state', () => { + expect(findGlEmptyState().props()).toMatchObject({ + description: IssuesListApp.i18n.noOpenIssuesDescription, + title: IssuesListApp.i18n.noOpenIssuesTitle, + svgPath: defaultProvide.emptyStateSvgPath, + }); + }); + }); + + describe('when "Closed" tab has no issues', () => { + beforeEach(() => { + setWindowLocation(`?state=${IssuableStates.Closed}`); + + wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount }); + }); + + it('shows empty state', () => { + expect(findGlEmptyState().props()).toMatchObject({ + title: IssuesListApp.i18n.noClosedIssuesTitle, + svgPath: defaultProvide.emptyStateSvgPath, + }); + }); + }); + }); + + describe('when there are no issues', () => { + describe('when user is logged in', () => { + beforeEach(() => { + wrapper = mountComponent({ + provide: { hasAnyIssues: false, isSignedIn: true }, + mountFn: mount, + }); + }); + + it('shows empty state', () => { + expect(findGlEmptyState().props()).toMatchObject({ + description: IssuesListApp.i18n.noIssuesSignedInDescription, + title: IssuesListApp.i18n.noIssuesSignedInTitle, + svgPath: defaultProvide.emptyStateSvgPath, + }); + }); + + it('shows "New issue" and import/export buttons', () => { + expect(findGlButton().text()).toBe(IssuesListApp.i18n.newIssueLabel); + expect(findGlButton().attributes('href')).toBe(defaultProvide.newIssuePath); + expect(findCsvImportExportButtons().props()).toMatchObject({ + exportCsvPath: defaultProvide.exportCsvPath, + issuableCount: 0, + }); + }); + + it('shows Jira integration information', () => { + const paragraphs = wrapper.findAll('p'); + expect(paragraphs.at(1).text()).toContain(IssuesListApp.i18n.jiraIntegrationTitle); + expect(paragraphs.at(2).text()).toContain( + 'Enable the Jira integration to view your Jira issues in GitLab.', + ); + expect(paragraphs.at(3).text()).toContain( + IssuesListApp.i18n.jiraIntegrationSecondaryMessage, + ); + expect(findGlLink().text()).toBe('Enable the Jira integration'); + expect(findGlLink().attributes('href')).toBe(defaultProvide.jiraIntegrationPath); + }); + }); + + describe('when user is logged out', () => { + beforeEach(() => { + wrapper = mountComponent({ + provide: { hasAnyIssues: false, isSignedIn: false }, + }); + }); + + it('shows empty state', () => { + expect(findGlEmptyState().props()).toMatchObject({ + description: IssuesListApp.i18n.noIssuesSignedOutDescription, + title: IssuesListApp.i18n.noIssuesSignedOutTitle, + svgPath: defaultProvide.emptyStateSvgPath, + primaryButtonText: IssuesListApp.i18n.noIssuesSignedOutButtonText, + primaryButtonLink: defaultProvide.signInPath, + }); + }); + }); + }); + }); + + describe('tokens', () => { + const mockCurrentUser = { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: 'avatar/url', + }; + + describe('when user is signed out', () => { + beforeEach(() => { + wrapper = mountComponent({ provide: { isSignedIn: false } }); + }); + + it('does not render My-Reaction or Confidential tokens', () => { + expect(findIssuableList().props('searchTokens')).not.toMatchObject([ + { type: TOKEN_TYPE_AUTHOR, preloadedAuthors: [mockCurrentUser] }, + { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors: [mockCurrentUser] }, + { type: TOKEN_TYPE_MY_REACTION }, + { type: TOKEN_TYPE_CONFIDENTIAL }, + ]); + }); + }); + + describe('when all tokens are available', () => { + const originalGon = window.gon; + + beforeEach(() => { + window.gon = { + ...originalGon, + current_user_id: mockCurrentUser.id, + current_user_fullname: mockCurrentUser.name, + current_username: mockCurrentUser.username, + current_user_avatar_url: mockCurrentUser.avatar_url, + }; + + wrapper = mountComponent({ provide: { isSignedIn: true } }); + }); + + afterEach(() => { + window.gon = originalGon; + }); + + it('renders all tokens alphabetically', () => { + const preloadedAuthors = [ + { ...mockCurrentUser, id: convertToGraphQLId('User', mockCurrentUser.id) }, + ]; + + expect(findIssuableList().props('searchTokens')).toMatchObject([ + { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors }, + { type: TOKEN_TYPE_AUTHOR, preloadedAuthors }, + { type: TOKEN_TYPE_CONFIDENTIAL }, + { type: TOKEN_TYPE_LABEL }, + { type: TOKEN_TYPE_MILESTONE }, + { type: TOKEN_TYPE_MY_REACTION }, + { type: TOKEN_TYPE_RELEASE }, + { type: TOKEN_TYPE_TYPE }, + ]); + }); + }); + }); + + describe('errors', () => { + describe.each` + error | mountOption | message + ${'fetching issues'} | ${'issuesQueryResponse'} | ${IssuesListApp.i18n.errorFetchingIssues} + ${'fetching issue counts'} | ${'issuesCountsQueryResponse'} | ${IssuesListApp.i18n.errorFetchingCounts} + `('when there is an error $error', ({ mountOption, message }) => { + beforeEach(() => { + wrapper = mountComponent({ + [mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')), + }); + jest.runOnlyPendingTimers(); + }); + + it('shows an error message', () => { + expect(findIssuableList().props('error')).toBe(message); + expect(Sentry.captureException).toHaveBeenCalledWith(new Error('Network error: ERROR')); + }); + }); + + it('clears error message when "dismiss-alert" event is emitted from IssuableList', () => { + wrapper = mountComponent({ issuesQueryResponse: jest.fn().mockRejectedValue(new Error()) }); + + findIssuableList().vm.$emit('dismiss-alert'); + + expect(findIssuableList().props('error')).toBeNull(); + }); + }); + + describe('events', () => { + describe('when "click-tab" event is emitted by IssuableList', () => { + beforeEach(() => { + wrapper = mountComponent(); + + findIssuableList().vm.$emit('click-tab', IssuableStates.Closed); + }); + + it('updates to the new tab', () => { + expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed); + }); + }); + + describe.each(['next-page', 'previous-page'])( + 'when "%s" event is emitted by IssuableList', + (event) => { + beforeEach(() => { + wrapper = mountComponent(); + + findIssuableList().vm.$emit(event); + }); + + it('scrolls to the top', () => { + expect(scrollUp).toHaveBeenCalled(); + }); + }, + ); + + describe('when "reorder" event is emitted by IssuableList', () => { + const issueOne = { + ...defaultQueryResponse.data.project.issues.nodes[0], + id: 'gid://gitlab/Issue/1', + iid: '101', + reference: 'group/project#1', + webPath: '/group/project/-/issues/1', + }; + const issueTwo = { + ...defaultQueryResponse.data.project.issues.nodes[0], + id: 'gid://gitlab/Issue/2', + iid: '102', + reference: 'group/project#2', + webPath: '/group/project/-/issues/2', + }; + const issueThree = { + ...defaultQueryResponse.data.project.issues.nodes[0], + id: 'gid://gitlab/Issue/3', + iid: '103', + reference: 'group/project#3', + webPath: '/group/project/-/issues/3', + }; + const issueFour = { + ...defaultQueryResponse.data.project.issues.nodes[0], + id: 'gid://gitlab/Issue/4', + iid: '104', + reference: 'group/project#4', + webPath: '/group/project/-/issues/4', + }; + const response = (isProject = true) => ({ + data: { + [isProject ? 'project' : 'group']: { + id: '1', + issues: { + ...defaultQueryResponse.data.project.issues, + nodes: [issueOne, issueTwo, issueThree, issueFour], + }, + }, + }, + }); + + describe('when successful', () => { + describe.each([true, false])('when isProject=%s', (isProject) => { + describe.each` + description | issueToMove | oldIndex | newIndex | moveBeforeId | moveAfterId + ${'to the beginning of the list'} | ${issueThree} | ${2} | ${0} | ${null} | ${issueOne.id} + ${'down the list'} | ${issueOne} | ${0} | ${1} | ${issueTwo.id} | ${issueThree.id} + ${'up the list'} | ${issueThree} | ${2} | ${1} | ${issueOne.id} | ${issueTwo.id} + ${'to the end of the list'} | ${issueTwo} | ${1} | ${3} | ${issueFour.id} | ${null} + `( + 'when moving issue $description', + ({ issueToMove, oldIndex, newIndex, moveBeforeId, moveAfterId }) => { + beforeEach(() => { + wrapper = mountComponent({ + provide: { isProject }, + issuesQueryResponse: jest.fn().mockResolvedValue(response(isProject)), + }); + jest.runOnlyPendingTimers(); + }); + + it('makes API call to reorder the issue', async () => { + findIssuableList().vm.$emit('reorder', { oldIndex, newIndex }); + + await waitForPromises(); + + expect(axiosMock.history.put[0]).toMatchObject({ + url: joinPaths(issueToMove.webPath, 'reorder'), + data: JSON.stringify({ + move_before_id: getIdFromGraphQLId(moveBeforeId), + move_after_id: getIdFromGraphQLId(moveAfterId), + group_full_path: isProject ? undefined : defaultProvide.fullPath, + }), + }); + }); + }, + ); + }); + }); + + describe('when unsuccessful', () => { + beforeEach(() => { + wrapper = mountComponent({ + issuesQueryResponse: jest.fn().mockResolvedValue(response()), + }); + jest.runOnlyPendingTimers(); + }); + + it('displays an error message', async () => { + axiosMock.onPut(joinPaths(issueOne.webPath, 'reorder')).reply(500); + + findIssuableList().vm.$emit('reorder', { oldIndex: 0, newIndex: 1 }); + + await waitForPromises(); + + expect(findIssuableList().props('error')).toBe(IssuesListApp.i18n.reorderError); + expect(Sentry.captureException).toHaveBeenCalledWith( + new Error('Request failed with status code 500'), + ); + }); + }); + }); + + describe('when "sort" event is emitted by IssuableList', () => { + it.each(Object.keys(urlSortParams))( + 'updates to the new sort when payload is `%s`', + async (sortKey) => { + wrapper = mountComponent(); + + findIssuableList().vm.$emit('sort', sortKey); + + jest.runOnlyPendingTimers(); + await nextTick(); + + expect(findIssuableList().props('urlParams')).toMatchObject({ + sort: urlSortParams[sortKey], + }); + }, + ); + + describe('when issue repositioning is disabled', () => { + const initialSort = CREATED_DESC; + + beforeEach(() => { + setWindowLocation(`?sort=${initialSort}`); + wrapper = mountComponent({ provide: { isIssueRepositioningDisabled: true } }); + + findIssuableList().vm.$emit('sort', RELATIVE_POSITION_ASC); + }); + + it('does not update the sort to manual', () => { + expect(findIssuableList().props('urlParams')).toMatchObject({ + sort: urlSortParams[initialSort], + }); + }); + + it('shows an alert to tell the user that manual reordering is disabled', () => { + expect(createFlash).toHaveBeenCalledWith({ + message: IssuesListApp.i18n.issueRepositioningMessage, + type: FLASH_TYPES.NOTICE, + }); + }); + }); + }); + + describe('when "update-legacy-bulk-edit" event is emitted by IssuableList', () => { + beforeEach(() => { + wrapper = mountComponent(); + jest.spyOn(eventHub, '$emit'); + + findIssuableList().vm.$emit('update-legacy-bulk-edit'); + }); + + it('emits an "issuables:updateBulkEdit" event to the legacy bulk edit class', () => { + expect(eventHub.$emit).toHaveBeenCalledWith('issuables:updateBulkEdit'); + }); + }); + + describe('when "filter" event is emitted by IssuableList', () => { + it('updates IssuableList with url params', async () => { + wrapper = mountComponent(); + + findIssuableList().vm.$emit('filter', filteredTokens); + await nextTick(); + + expect(findIssuableList().props('urlParams')).toMatchObject(urlParams); + }); + + describe('when anonymous searching is performed', () => { + beforeEach(() => { + wrapper = mountComponent({ + provide: { isAnonymousSearchDisabled: true, isSignedIn: false }, + }); + + findIssuableList().vm.$emit('filter', filteredTokens); + }); + + it('does not update IssuableList with url params ', async () => { + const defaultParams = { sort: 'created_date', state: 'opened' }; + + expect(findIssuableList().props('urlParams')).toEqual(defaultParams); + }); + + it('shows an alert to tell the user they must be signed in to search', () => { + expect(createFlash).toHaveBeenCalledWith({ + message: IssuesListApp.i18n.anonymousSearchingMessage, + type: FLASH_TYPES.NOTICE, + }); + }); + }); + }); + }); +}); |