diff options
Diffstat (limited to 'spec/frontend/issues')
34 files changed, 607 insertions, 838 deletions
diff --git a/spec/frontend/issues/create_merge_request_dropdown_spec.js b/spec/frontend/issues/create_merge_request_dropdown_spec.js index cc2ee84348a..21ae844e2dd 100644 --- a/spec/frontend/issues/create_merge_request_dropdown_spec.js +++ b/spec/frontend/issues/create_merge_request_dropdown_spec.js @@ -65,6 +65,14 @@ describe('CreateMergeRequestDropdown', () => { expect(dropdown.createMrPath).toBe( `${TEST_HOST}/create_merge_request?merge_request%5Bsource_branch%5D=contains%23hash&merge_request%5Btarget_branch%5D=master&merge_request%5Bissue_iid%5D=42`, ); + + expect(dropdown.wrapperEl.dataset.createBranchPath).toBe( + `${TEST_HOST}/branches?branch_name=contains%23hash&issue=42`, + ); + + expect(dropdown.wrapperEl.dataset.createMrPath).toBe( + `${TEST_HOST}/create_merge_request?merge_request%5Bsource_branch%5D=contains%23hash&merge_request%5Btarget_branch%5D=master&merge_request%5Bissue_iid%5D=42`, + ); }); }); 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 77d5a0579a4..ebf4771e97f 100644 --- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js +++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js @@ -18,6 +18,7 @@ import { setSortPreferenceMutationResponse, setSortPreferenceMutationResponseWithErrors, } from 'jest/issues/list/mock_data'; +import { STATUS_ALL, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants'; import IssuesDashboardApp from '~/issues/dashboard/components/issues_dashboard_app.vue'; import getIssuesCountsQuery from '~/issues/dashboard/queries/get_issues_counts.query.graphql'; import { CREATED_DESC, i18n, UPDATED_DESC, urlSortParams } from '~/issues/list/constants'; @@ -36,7 +37,6 @@ import { TOKEN_TYPE_TYPE, } 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'; import { emptyIssuesQueryResponse, issuesCountsQueryResponse, @@ -124,7 +124,7 @@ describe('IssuesDashboardApp component', () => { // eslint-disable-next-line jest/no-disabled-tests it.skip('renders IssuableList component', () => { expect(findIssuableList().props()).toMatchObject({ - currentTab: IssuableStates.Opened, + currentTab: STATUS_OPEN, hasNextPage: true, hasPreviousPage: false, hasScopedLabelsFeature: defaultProvide.hasScopedLabelsFeature, @@ -148,7 +148,7 @@ describe('IssuesDashboardApp component', () => { tabs: IssuesDashboardApp.IssuableListTabs, urlParams: { sort: urlSortParams[CREATED_DESC], - state: IssuableStates.Opened, + state: STATUS_OPEN, }, useKeysetPagination: true, }); @@ -283,7 +283,7 @@ describe('IssuesDashboardApp component', () => { describe('state', () => { it('is set from the url params', () => { - const initialState = IssuableStates.All; + const initialState = STATUS_ALL; setWindowLocation(`?state=${initialState}`); mountComponent(); @@ -337,11 +337,9 @@ describe('IssuesDashboardApp component', () => { username: 'root', avatar_url: 'avatar/url', }; - const originalGon = window.gon; beforeEach(() => { window.gon = { - ...originalGon, current_user_id: mockCurrentUser.id, current_user_fullname: mockCurrentUser.name, current_username: mockCurrentUser.username, @@ -350,10 +348,6 @@ describe('IssuesDashboardApp component', () => { mountComponent(); }); - afterEach(() => { - window.gon = originalGon; - }); - it('renders all tokens alphabetically', () => { const preloadedUsers = [{ ...mockCurrentUser, id: mockCurrentUser.id }]; @@ -375,16 +369,16 @@ describe('IssuesDashboardApp component', () => { beforeEach(() => { mountComponent(); - findIssuableList().vm.$emit('click-tab', IssuableStates.Closed); + findIssuableList().vm.$emit('click-tab', STATUS_CLOSED); }); it('updates ui to the new tab', () => { - expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed); + expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED); }); it('updates url to the new tab', () => { expect(findIssuableList().props('urlParams')).toMatchObject({ - state: IssuableStates.Closed, + state: STATUS_CLOSED, }); }); }); diff --git a/spec/frontend/issues/list/components/issue_card_time_info_spec.js b/spec/frontend/issues/list/components/issue_card_time_info_spec.js index ab4d023ee39..e80ffea0591 100644 --- a/spec/frontend/issues/list/components/issue_card_time_info_spec.js +++ b/spec/frontend/issues/list/components/issue_card_time_info_spec.js @@ -45,10 +45,6 @@ describe('CE IssueCardTimeInfo component', () => { }, }); - afterEach(() => { - wrapper.destroy(); - }); - describe('milestone', () => { it('renders', () => { wrapper = mountComponent(); diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js index 8281ce0ed1a..b28a08e2fce 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -15,19 +15,21 @@ import waitForPromises from 'helpers/wait_for_promises'; import { getIssuesCountsQueryResponse, getIssuesQueryResponse, + getIssuesQueryEmptyResponse, filteredTokens, locationSearch, setSortPreferenceMutationResponse, setSortPreferenceMutationResponseWithErrors, urlParams, } from 'jest/issues/list/mock_data'; -import { createAlert, VARIANT_INFO } from '~/flash'; +import { createAlert, VARIANT_INFO } from '~/alert'; import { TYPENAME_USER } from '~/graphql_shared/constants'; import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { STATUS_ALL, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants'; 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 { IssuableListTabs } from '~/vue_shared/issuable/list/constants'; import EmptyStateWithAnyIssues from '~/issues/list/components/empty_state_with_any_issues.vue'; import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue'; import IssuesListApp from '~/issues/list/components/issues_list_app.vue'; @@ -70,7 +72,7 @@ import('~/issuable'); import('~/users_select'); jest.mock('@sentry/browser'); -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() })); describe('CE IssuesListApp component', () => { @@ -154,7 +156,24 @@ describe('CE IssuesListApp component', () => { router = new VueRouter({ mode: 'history' }); return mountFn(IssuesListApp, { - apolloProvider: createMockApollo(requestHandlers), + apolloProvider: createMockApollo( + requestHandlers, + {}, + { + typePolicies: { + Query: { + fields: { + project: { + merge: true, + }, + group: { + merge: true, + }, + }, + }, + }, + }, + ), router, provide: { ...defaultProvide, @@ -174,13 +193,11 @@ describe('CE IssuesListApp component', () => { afterEach(() => { axiosMock.reset(); - wrapper.destroy(); }); describe('IssuableList', () => { beforeEach(() => { wrapper = mountComponent(); - jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -197,7 +214,7 @@ describe('CE IssuesListApp component', () => { initialSortBy: CREATED_DESC, issuables: getIssuesQueryResponse.data.project.issues.nodes, tabs: IssuableListTabs, - currentTab: IssuableStates.Opened, + currentTab: STATUS_OPEN, tabCounts: { opened: 1, closed: 1, @@ -247,7 +264,6 @@ describe('CE IssuesListApp component', () => { mountFn: mount, }); - jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -416,7 +432,7 @@ describe('CE IssuesListApp component', () => { describe('state', () => { it('is set from the url params', () => { - const initialState = IssuableStates.All; + const initialState = STATUS_ALL; setWindowLocation(`?state=${initialState}`); wrapper = mountComponent(); @@ -477,7 +493,12 @@ describe('CE IssuesListApp component', () => { describe('empty states', () => { describe('when there are issues', () => { beforeEach(() => { - wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount }); + wrapper = mountComponent({ + provide: { hasAnyIssues: true }, + mountFn: mount, + issuesQueryResponse: getIssuesQueryEmptyResponse, + }); + return waitForPromises(); }); it('shows EmptyStateWithAnyIssues empty state', () => { @@ -543,11 +564,8 @@ describe('CE IssuesListApp component', () => { }); 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, @@ -563,10 +581,6 @@ describe('CE IssuesListApp component', () => { }); }); - afterEach(() => { - window.gon = originalGon; - }); - it('renders all tokens alphabetically', () => { const preloadedUsers = [ { ...mockCurrentUser, id: convertToGraphQLId(TYPENAME_USER, mockCurrentUser.id) }, @@ -599,7 +613,6 @@ describe('CE IssuesListApp component', () => { wrapper = mountComponent({ [mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')), }); - jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -620,20 +633,21 @@ describe('CE IssuesListApp component', () => { describe('events', () => { describe('when "click-tab" event is emitted by IssuableList', () => { - beforeEach(() => { + beforeEach(async () => { wrapper = mountComponent(); + await waitForPromises(); router.push = jest.fn(); - findIssuableList().vm.$emit('click-tab', IssuableStates.Closed); + findIssuableList().vm.$emit('click-tab', STATUS_CLOSED); }); it('updates ui to the new tab', () => { - expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed); + expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED); }); it('updates url to the new tab', () => { expect(router.push).toHaveBeenCalledWith({ - query: expect.objectContaining({ state: IssuableStates.Closed }), + query: expect.objectContaining({ state: STATUS_CLOSED }), }); }); }); @@ -641,19 +655,25 @@ describe('CE IssuesListApp component', () => { describe.each` event | params ${'next-page'} | ${{ - page_after: 'endCursor', + page_after: 'endcursor', page_before: undefined, first_page_size: 20, last_page_size: undefined, + search: undefined, + sort: 'created_date', + state: 'opened', }} ${'previous-page'} | ${{ page_after: undefined, - page_before: 'startCursor', + page_before: 'startcursor', first_page_size: undefined, last_page_size: 20, + search: undefined, + sort: 'created_date', + state: 'opened', }} `('when "$event" event is emitted by IssuableList', ({ event, params }) => { - beforeEach(() => { + beforeEach(async () => { wrapper = mountComponent({ data: { pageInfo: { @@ -662,6 +682,7 @@ describe('CE IssuesListApp component', () => { }, }, }); + await waitForPromises(); router.push = jest.fn(); findIssuableList().vm.$emit(event); @@ -735,7 +756,6 @@ describe('CE IssuesListApp component', () => { provide: { isProject }, issuesQueryResponse: jest.fn().mockResolvedValue(response(isProject)), }); - jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -761,7 +781,6 @@ describe('CE IssuesListApp component', () => { wrapper = mountComponent({ issuesQueryResponse: jest.fn().mockResolvedValue(response()), }); - jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -793,8 +812,6 @@ describe('CE IssuesListApp component', () => { router.push = jest.fn(); findIssuableList().vm.$emit('sort', sortKey); - jest.runOnlyPendingTimers(); - await nextTick(); expect(router.push).toHaveBeenCalledWith({ query: expect.objectContaining({ sort: urlSortParams[sortKey] }), @@ -914,13 +931,13 @@ describe('CE IssuesListApp component', () => { ${'shows users when public visibility is not restricted and is signed in'} | ${false} | ${true} | ${false} ${'hides users when public visibility is restricted and is not signed in'} | ${true} | ${false} | ${true} ${'shows users when public visibility is restricted and is signed in'} | ${true} | ${true} | ${false} - `('$description', ({ isPublicVisibilityRestricted, isSignedIn, hideUsers }) => { + `('$description', async ({ isPublicVisibilityRestricted, isSignedIn, hideUsers }) => { const mockQuery = jest.fn().mockResolvedValue(defaultQueryResponse); wrapper = mountComponent({ provide: { isPublicVisibilityRestricted, isSignedIn }, issuesQueryResponse: mockQuery, }); - jest.runOnlyPendingTimers(); + await waitForPromises(); expect(mockQuery).toHaveBeenCalledWith(expect.objectContaining({ hideUsers })); }); @@ -929,7 +946,6 @@ describe('CE IssuesListApp component', () => { describe('fetching issues', () => { beforeEach(() => { wrapper = mountComponent(); - jest.runOnlyPendingTimers(); }); it('fetches issue, incident, test case, and task types', () => { diff --git a/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js index 406b1fbc1af..7bbb5a954ae 100644 --- a/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js +++ b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js @@ -38,11 +38,6 @@ describe('JiraIssuesImportStatus', () => { }, }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when Jira import is neither in progress nor finished', () => { beforeEach(() => { wrapper = mountComponent(); diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js index 1e8a81116f3..0332f68ddb6 100644 --- a/spec/frontend/issues/list/mock_data.js +++ b/spec/frontend/issues/list/mock_data.js @@ -101,6 +101,26 @@ export const getIssuesQueryResponse = { }, }; +export const getIssuesQueryEmptyResponse = { + data: { + project: { + id: '1', + __typename: 'Project', + issues: { + __persist: true, + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + hasPreviousPage: false, + startCursor: 'startcursor', + endCursor: 'endcursor', + }, + nodes: [], + }, + }, + }, +}; + export const getIssuesCountsQueryResponse = { data: { project: { diff --git a/spec/frontend/issues/list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js index a281ed1c989..e4ecdc6c29e 100644 --- a/spec/frontend/issues/list/utils_spec.js +++ b/spec/frontend/issues/list/utils_spec.js @@ -10,7 +10,7 @@ import { urlParams, urlParamsWithSpecialValues, } from 'jest/issues/list/mock_data'; -import { PAGE_SIZE, urlSortParams } from '~/issues/list/constants'; +import { urlSortParams } from '~/issues/list/constants'; import { convertToApiParams, convertToSearchQuery, @@ -22,10 +22,11 @@ import { isSortKey, } from '~/issues/list/utils'; import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; +import { DEFAULT_PAGE_SIZE } from '~/vue_shared/issuable/list/constants'; describe('getInitialPageParams', () => { it('returns page params with a default page size when no arguments are given', () => { - expect(getInitialPageParams()).toEqual({ firstPageSize: PAGE_SIZE }); + expect(getInitialPageParams()).toEqual({ firstPageSize: DEFAULT_PAGE_SIZE }); }); it('returns page params with the given page size', () => { diff --git a/spec/frontend/issues/new/components/title_suggestions_item_spec.js b/spec/frontend/issues/new/components/title_suggestions_item_spec.js index c54a762440f..4454ef81416 100644 --- a/spec/frontend/issues/new/components/title_suggestions_item_spec.js +++ b/spec/frontend/issues/new/components/title_suggestions_item_spec.js @@ -25,10 +25,6 @@ describe('Issue title suggestions item component', () => { const findTooltip = () => wrapper.findComponent(GlTooltip); const findUserAvatar = () => wrapper.findComponent(UserAvatarImage); - afterEach(() => { - wrapper.destroy(); - }); - it('renders title', () => { createComponent(); diff --git a/spec/frontend/issues/new/components/title_suggestions_spec.js b/spec/frontend/issues/new/components/title_suggestions_spec.js index 1cd6576967a..343bdbba301 100644 --- a/spec/frontend/issues/new/components/title_suggestions_spec.js +++ b/spec/frontend/issues/new/components/title_suggestions_spec.js @@ -1,106 +1,95 @@ import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import TitleSuggestions from '~/issues/new/components/title_suggestions.vue'; import TitleSuggestionsItem from '~/issues/new/components/title_suggestions_item.vue'; +import getIssueSuggestionsQuery from '~/issues/new/queries/issues.query.graphql'; +import { mockIssueSuggestionResponse } from '../mock_data'; + +Vue.use(VueApollo); + +const MOCK_PROJECT_PATH = 'project'; +const MOCK_ISSUES_COUNT = mockIssueSuggestionResponse.data.project.issues.edges.length; describe('Issue title suggestions component', () => { let wrapper; + let mockApollo; + + function createComponent({ + search = 'search', + queryResponse = jest.fn().mockResolvedValue(mockIssueSuggestionResponse), + } = {}) { + mockApollo = createMockApollo([[getIssueSuggestionsQuery, queryResponse]]); - function createComponent(search = 'search') { wrapper = shallowMount(TitleSuggestions, { propsData: { search, - projectPath: 'project', + projectPath: MOCK_PROJECT_PATH, }, + apolloProvider: mockApollo, }); } - beforeEach(() => { - createComponent(); - }); + const waitForDebounce = () => { + jest.runOnlyPendingTimers(); + return waitForPromises(); + }; afterEach(() => { - wrapper.destroy(); + mockApollo = null; }); it('does not render with empty search', async () => { - wrapper.setProps({ search: '' }); + createComponent({ search: '' }); + await waitForDebounce(); - await nextTick(); expect(wrapper.isVisible()).toBe(false); }); - describe('with data', () => { - let data; - - beforeEach(() => { - data = { issues: [{ id: 1 }, { id: 2 }] }; - }); - - it('renders component', async () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData(data); - - await nextTick(); - expect(wrapper.findAll('li').length).toBe(data.issues.length); - }); + it('does not render when loading', () => { + createComponent(); + expect(wrapper.isVisible()).toBe(false); + }); - it('does not render with empty search', async () => { - wrapper.setProps({ search: '' }); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData(data); + it('does not render with empty issues data', async () => { + const emptyIssuesResponse = { + data: { + project: { + id: 'gid://gitlab/Project/1', + issues: { + edges: [], + }, + }, + }, + }; - await nextTick(); - expect(wrapper.isVisible()).toBe(false); - }); + createComponent({ queryResponse: jest.fn().mockResolvedValue(emptyIssuesResponse) }); + await waitForDebounce(); - it('does not render when loading', async () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - ...data, - loading: 1, - }); + expect(wrapper.isVisible()).toBe(false); + }); - await nextTick(); - expect(wrapper.isVisible()).toBe(false); + describe('with data', () => { + beforeEach(async () => { + createComponent(); + await waitForDebounce(); }); - it('does not render with empty issues data', async () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ issues: [] }); - - await nextTick(); - expect(wrapper.isVisible()).toBe(false); + it('renders component', () => { + expect(wrapper.findAll('li').length).toBe(MOCK_ISSUES_COUNT); }); - it('renders list of issues', async () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData(data); - - await nextTick(); - expect(wrapper.findAllComponents(TitleSuggestionsItem).length).toBe(2); + it('renders list of issues', () => { + expect(wrapper.findAllComponents(TitleSuggestionsItem).length).toBe(MOCK_ISSUES_COUNT); }); - it('adds margin class to first item', async () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData(data); - - await nextTick(); + it('adds margin class to first item', () => { expect(wrapper.findAll('li').at(0).classes()).toContain('gl-mb-3'); }); - it('does not add margin class to last item', async () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData(data); - - await nextTick(); + it('does not add margin class to last item', () => { expect(wrapper.findAll('li').at(1).classes()).not.toContain('gl-mb-3'); }); }); diff --git a/spec/frontend/issues/new/components/type_popover_spec.js b/spec/frontend/issues/new/components/type_popover_spec.js index fe3d5207516..1ae150797c3 100644 --- a/spec/frontend/issues/new/components/type_popover_spec.js +++ b/spec/frontend/issues/new/components/type_popover_spec.js @@ -8,10 +8,6 @@ describe('Issue type info popover', () => { wrapper = shallowMount(TypePopover); } - afterEach(() => { - wrapper.destroy(); - }); - it('renders', () => { createComponent(); diff --git a/spec/frontend/issues/new/mock_data.js b/spec/frontend/issues/new/mock_data.js index 74b569d9833..0d2a388cd86 100644 --- a/spec/frontend/issues/new/mock_data.js +++ b/spec/frontend/issues/new/mock_data.js @@ -26,3 +26,67 @@ export default () => ({ webUrl: `${TEST_HOST}/author`, }, }); + +export const mockIssueSuggestionResponse = { + data: { + project: { + id: 'gid://gitlab/Project/278964', + issues: { + edges: [ + { + node: { + id: 'gid://gitlab/Issue/123725957', + iid: '696', + title: 'Remove unused MR widget extension expand success, failed, warning events', + confidential: false, + userNotesCount: 16, + upvotes: 0, + webUrl: 'https://gitlab.com/gitlab-org/gitlab/-/issues/696', + state: 'opened', + closedAt: null, + createdAt: '2023-02-15T12:29:59Z', + updatedAt: '2023-03-01T19:38:22Z', + author: { + id: 'gid://gitlab/User/325', + name: 'User Name', + username: 'user-name', + avatarUrl: '/uploads/-/system/user/avatar/325/avatar.png', + webUrl: 'https://gitlab.com/user-name', + __typename: 'UserCore', + }, + __typename: 'Issue', + }, + __typename: 'IssueEdge', + }, + { + node: { + id: 'gid://gitlab/Issue/123', + iid: '391', + title: 'Remove unused MR widget extension expand success, failed, warning events', + confidential: false, + userNotesCount: 16, + upvotes: 0, + webUrl: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391', + state: 'opened', + closedAt: null, + createdAt: '2023-02-15T12:29:59Z', + updatedAt: '2023-03-01T19:38:22Z', + author: { + id: 'gid://gitlab/User/2080', + name: 'User Name', + username: 'user-name', + avatarUrl: '/uploads/-/system/user/avatar/2080/avatar.png', + webUrl: 'https://gitlab.com/user-name', + __typename: 'UserCore', + }, + __typename: 'Issue', + }, + __typename: 'IssueEdge', + }, + ], + __typename: 'IssueConnection', + }, + __typename: 'Project', + }, + }, +}; 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 010c719bd84..c5507c88fd7 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 @@ -34,7 +34,6 @@ describe('RelatedMergeRequests', () => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); }); diff --git a/spec/frontend/issues/related_merge_requests/store/actions_spec.js b/spec/frontend/issues/related_merge_requests/store/actions_spec.js index 7339372a8d1..31c96265f8d 100644 --- a/spec/frontend/issues/related_merge_requests/store/actions_spec.js +++ b/spec/frontend/issues/related_merge_requests/store/actions_spec.js @@ -1,12 +1,12 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import * as actions from '~/issues/related_merge_requests/store/actions'; import * as types from '~/issues/related_merge_requests/store/mutation_types'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('RelatedMergeRequest store actions', () => { let state; diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js index 9fa0ce6f93d..1006f54eeaf 100644 --- a/spec/frontend/issues/show/components/app_spec.js +++ b/spec/frontend/issues/show/components/app_spec.js @@ -1,11 +1,10 @@ import { GlIcon, GlIntersectionObserver } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; -import { nextTick } from 'vue'; -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { setHTMLFixture } from 'helpers/fixtures'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { IssuableStatusText, STATUS_CLOSED, @@ -21,29 +20,27 @@ import FormComponent from '~/issues/show/components/form.vue'; import TitleComponent from '~/issues/show/components/title.vue'; import IncidentTabs from '~/issues/show/components/incidents/incident_tabs.vue'; import PinnedLinks from '~/issues/show/components/pinned_links.vue'; -import { POLLING_DELAY } from '~/issues/show/constants'; import eventHub from '~/issues/show/event_hub'; import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import { HTTP_STATUS_OK, HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status'; import { visitUrl } from '~/lib/utils/url_utility'; import { appProps, initialRequest, publishedIncidentUrl, + putRequest, secondRequest, zoomMeetingUrl, } from '../mock_data/mock_data'; -jest.mock('~/flash'); -jest.mock('~/issues/show/event_hub'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility'); jest.mock('~/behaviors/markdown/render_gfm'); const REALTIME_REQUEST_STACK = [initialRequest, secondRequest]; describe('Issuable output', () => { - let mock; - let realtimeRequestCount = 0; + let axiosMock; let wrapper; const findStickyHeader = () => wrapper.findByTestId('issue-sticky-header'); @@ -57,15 +54,14 @@ describe('Issuable output', () => { const findForm = () => wrapper.findComponent(FormComponent); const findPinnedLinks = () => wrapper.findComponent(PinnedLinks); - const mountComponent = (props = {}, options = {}, data = {}) => { + const createComponent = ({ props = {}, options = {}, data = {} } = {}) => { wrapper = shallowMountExtended(IssuableApp, { directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, propsData: { ...appProps, ...props }, provide: { fullPath: 'gitlab-org/incidents', - iid: '19', uploadMetricsFeatureAvailable: false, }, stubs: { @@ -79,6 +75,28 @@ describe('Issuable output', () => { }, ...options, }); + + jest.advanceTimersToNextTimer(2); + return waitForPromises(); + }; + + const emitHubEvent = (event) => { + eventHub.$emit(event); + return waitForPromises(); + }; + + const openForm = () => { + return emitHubEvent('open.form'); + }; + + const updateIssuable = () => { + return emitHubEvent('update.issuable'); + }; + + const advanceToNextPoll = () => { + // We get new data through the HTTP request. + jest.advanceTimersToNextTimer(); + return waitForPromises(); }; beforeEach(() => { @@ -98,79 +116,100 @@ describe('Issuable output', () => { </div> `); - mock = new MockAdapter(axios); - mock - .onGet('/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes') - .reply(() => { - const res = Promise.resolve([HTTP_STATUS_OK, REALTIME_REQUEST_STACK[realtimeRequestCount]]); - realtimeRequestCount += 1; - return res; - }); + jest.spyOn(eventHub, '$emit'); - mountComponent(); + axiosMock = new MockAdapter(axios); + const endpoint = '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes'; - jest.advanceTimersByTime(2); + axiosMock.onGet(endpoint).replyOnce(HTTP_STATUS_OK, REALTIME_REQUEST_STACK[0], { + 'POLL-INTERVAL': '1', + }); + axiosMock.onGet(endpoint).reply(HTTP_STATUS_OK, REALTIME_REQUEST_STACK[1], { + 'POLL-INTERVAL': '-1', + }); + axiosMock.onPut().reply(HTTP_STATUS_OK, putRequest); }); - afterEach(() => { - mock.restore(); - realtimeRequestCount = 0; - wrapper.vm.poll.stop(); - wrapper.destroy(); - resetHTMLFixture(); - }); + describe('update', () => { + beforeEach(async () => { + await createComponent(); + }); - it('should render a title/description/edited and update title/description/edited on update', () => { - return axios - .waitForAll() - .then(() => { - expect(findTitle().props('titleText')).toContain('this is a title'); - expect(findDescription().props('descriptionText')).toContain('this is a description'); - - expect(findEdited().exists()).toBe(true); - expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/); - expect(findEdited().props('updatedAt')).toBe(initialRequest.updated_at); - expect(wrapper.vm.state.lock_version).toBe(initialRequest.lock_version); - }) - .then(() => { - wrapper.vm.poll.makeRequest(); - return axios.waitForAll(); - }) - .then(() => { - expect(findTitle().props('titleText')).toContain('2'); - expect(findDescription().props('descriptionText')).toContain('42'); - - expect(findEdited().exists()).toBe(true); - expect(findEdited().props('updatedByName')).toBe('Other User'); - expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/); - expect(findEdited().props('updatedAt')).toBe(secondRequest.updated_at); - }); - }); + it('should render a title/description/edited and update title/description/edited on update', async () => { + expect(findTitle().props('titleText')).toContain(initialRequest.title_text); + expect(findDescription().props('descriptionText')).toContain('this is a description'); - it('shows actions if permissions are correct', async () => { - wrapper.vm.showForm = true; + expect(findEdited().exists()).toBe(true); + expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/); + expect(findEdited().props('updatedAt')).toBe(initialRequest.updated_at); + expect(findDescription().props().lockVersion).toBe(initialRequest.lock_version); - await nextTick(); - expect(findForm().exists()).toBe(true); - }); + await advanceToNextPoll(); - it('does not show actions if permissions are incorrect', async () => { - wrapper.vm.showForm = true; - wrapper.setProps({ canUpdate: false }); + expect(findTitle().props('titleText')).toContain('2'); + expect(findDescription().props('descriptionText')).toContain('42'); - await nextTick(); - expect(findForm().exists()).toBe(false); + expect(findEdited().exists()).toBe(true); + expect(findEdited().props('updatedByName')).toBe('Other User'); + expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/); + expect(findEdited().props('updatedAt')).toBe(secondRequest.updated_at); + }); }); - it('does not update formState if form is already open', async () => { - wrapper.vm.updateAndShowForm(); + describe('with permissions', () => { + beforeEach(async () => { + await createComponent(); + }); - wrapper.vm.state.titleText = 'testing 123'; + it('shows actions on `open.form` event', async () => { + expect(findForm().exists()).toBe(false); - wrapper.vm.updateAndShowForm(); + await openForm(); - await nextTick(); - expect(wrapper.vm.store.formState.title).not.toBe('testing 123'); + expect(findForm().exists()).toBe(true); + }); + + it('update formState if form is not open', async () => { + const titleValue = initialRequest.title_text; + + expect(findTitle().exists()).toBe(true); + expect(findTitle().props('titleText')).toBe(titleValue); + + await advanceToNextPoll(); + + // The title component has the new data, so the state was updated + expect(findTitle().exists()).toBe(true); + expect(findTitle().props('titleText')).toBe(secondRequest.title_text); + }); + + it('does not update formState if form is already open', async () => { + const titleValue = initialRequest.title_text; + + expect(findTitle().exists()).toBe(true); + expect(findTitle().props('titleText')).toBe(titleValue); + + await openForm(); + + // Opening the form, the data has not changed + expect(findForm().props().formState.title).toBe(titleValue); + + await advanceToNextPoll(); + + // We expect the prop value not to have changed after another API call + expect(findForm().props().formState.title).toBe(titleValue); + }); + }); + + describe('without permissions', () => { + beforeEach(async () => { + await createComponent({ props: { canUpdate: false } }); + }); + + it('does not show actions if permissions are incorrect', async () => { + await openForm(); + + expect(findForm().exists()).toBe(false); + }); }); describe('Pinned links propagated', () => { @@ -178,288 +217,130 @@ describe('Issuable output', () => { prop | value ${'zoomMeetingUrl'} | ${zoomMeetingUrl} ${'publishedIncidentUrl'} | ${publishedIncidentUrl} - `('sets the $prop correctly on underlying pinned links', ({ prop, value }) => { + `('sets the $prop correctly on underlying pinned links', async ({ prop, value }) => { + await createComponent(); + expect(findPinnedLinks().props(prop)).toBe(value); }); }); - describe('updateIssuable', () => { + describe('updating an issue', () => { + beforeEach(async () => { + await createComponent(); + }); + it('fetches new data after update', async () => { - const updateStoreSpy = jest.spyOn(wrapper.vm, 'updateStoreState'); - const getDataSpy = jest.spyOn(wrapper.vm.service, 'getData'); - jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ - data: { web_url: window.location.pathname }, - }); + await advanceToNextPoll(); - await wrapper.vm.updateIssuable(); - expect(updateStoreSpy).toHaveBeenCalled(); - expect(getDataSpy).toHaveBeenCalled(); + await updateIssuable(); + + expect(axiosMock.history.put).toHaveLength(1); + // The call was made with the new data + expect(axiosMock.history.put[0].data.title).toEqual(findTitle().props().title); }); - it('correctly updates issuable data', async () => { - const spy = jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ - data: { web_url: window.location.pathname }, - }); + it('closes the form after fetching data', async () => { + await updateIssuable(); - await wrapper.vm.updateIssuable(); - expect(spy).toHaveBeenCalledWith(wrapper.vm.formState); expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); }); it('does not redirect if issue has not moved', async () => { - jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ - data: { - web_url: window.location.pathname, - confidential: wrapper.vm.isConfidential, - }, + axiosMock.onPut().reply(HTTP_STATUS_OK, { + ...putRequest, + confidential: appProps.isConfidential, }); - await wrapper.vm.updateIssuable(); + await updateIssuable(); + expect(visitUrl).not.toHaveBeenCalled(); }); it('does not redirect if issue has not moved and user has switched tabs', async () => { - jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ - data: { - web_url: '', - confidential: wrapper.vm.isConfidential, - }, + axiosMock.onPut().reply(HTTP_STATUS_OK, { + ...putRequest, + web_url: '', + confidential: appProps.isConfidential, }); - await wrapper.vm.updateIssuable(); + await updateIssuable(); + expect(visitUrl).not.toHaveBeenCalled(); }); it('redirects if returned web_url has changed', async () => { - jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ - data: { - web_url: '/testing-issue-move', - confidential: wrapper.vm.isConfidential, - }, - }); - - wrapper.vm.updateIssuable(); - - await wrapper.vm.updateIssuable(); - expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move'); - }); - - describe('shows dialog when issue has unsaved changed', () => { - it('confirms on title change', async () => { - wrapper.vm.showForm = true; - wrapper.vm.state.titleText = 'title has changed'; - const e = { returnValue: null }; - wrapper.vm.handleBeforeUnloadEvent(e); - - await nextTick(); - expect(e.returnValue).not.toBeNull(); - }); - - it('confirms on description change', async () => { - wrapper.vm.showForm = true; - wrapper.vm.state.descriptionText = 'description has changed'; - const e = { returnValue: null }; - wrapper.vm.handleBeforeUnloadEvent(e); + const webUrl = '/testing-issue-move'; - await nextTick(); - expect(e.returnValue).not.toBeNull(); + axiosMock.onPut().reply(HTTP_STATUS_OK, { + ...putRequest, + web_url: webUrl, + confidential: appProps.isConfidential, }); - it('does nothing when nothing has changed', async () => { - const e = { returnValue: null }; - wrapper.vm.handleBeforeUnloadEvent(e); + await updateIssuable(); - await nextTick(); - expect(e.returnValue).toBeNull(); - }); + expect(visitUrl).toHaveBeenCalledWith(webUrl); }); describe('error when updating', () => { - it('closes form on error', async () => { - jest.spyOn(wrapper.vm.service, 'updateIssuable').mockRejectedValue(); + it('closes form', async () => { + axiosMock.onPut().reply(HTTP_STATUS_UNAUTHORIZED); - await wrapper.vm.updateIssuable(); - expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); - expect(createAlert).toHaveBeenCalledWith({ message: `Error updating issue` }); - }); + await updateIssuable(); - it('returns the correct error message for issuableType', async () => { - jest.spyOn(wrapper.vm.service, 'updateIssuable').mockRejectedValue(); - wrapper.setProps({ issuableType: 'merge request' }); - - await nextTick(); - await wrapper.vm.updateIssuable(); expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); - expect(createAlert).toHaveBeenCalledWith({ message: `Error updating merge request` }); - }); - - it('shows error message from backend if exists', async () => { - const msg = 'Custom error message from backend'; - jest - .spyOn(wrapper.vm.service, 'updateIssuable') - .mockRejectedValue({ response: { data: { errors: [msg] } } }); - - await wrapper.vm.updateIssuable(); expect(createAlert).toHaveBeenCalledWith({ - message: `${wrapper.vm.defaultErrorMessage}. ${msg}`, + message: `Error updating issue. Request failed with status code 401`, }); }); - }); - }); - - describe('updateAndShowForm', () => { - it('shows locked warning if form is open & data is different', async () => { - await nextTick(); - wrapper.vm.updateAndShowForm(); - - wrapper.vm.poll.makeRequest(); - await new Promise((resolve) => { - wrapper.vm.$watch('formState.lockedWarningVisible', (value) => { - if (value) { - resolve(); - } - }); - }); - - expect(wrapper.vm.formState.lockedWarningVisible).toBe(true); - expect(wrapper.vm.formState.lock_version).toBe(1); - }); - }); - - describe('requestTemplatesAndShowForm', () => { - let formSpy; - - beforeEach(() => { - formSpy = jest.spyOn(wrapper.vm, 'updateAndShowForm'); - }); - - it('shows the form if template names as hash request is successful', () => { - const mockData = { - test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }], - }; - mock - .onGet('/issuable-templates-path') - .reply(() => Promise.resolve([HTTP_STATUS_OK, mockData])); - - return wrapper.vm.requestTemplatesAndShowForm().then(() => { - expect(formSpy).toHaveBeenCalledWith(mockData); - }); - }); + it('returns the correct error message for issuableType', async () => { + axiosMock.onPut().reply(HTTP_STATUS_UNAUTHORIZED); - it('shows the form if template names as array request is successful', () => { - const mockData = [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }]; - mock - .onGet('/issuable-templates-path') - .reply(() => Promise.resolve([HTTP_STATUS_OK, mockData])); + await updateIssuable(); - return wrapper.vm.requestTemplatesAndShowForm().then(() => { - expect(formSpy).toHaveBeenCalledWith(mockData); - }); - }); - - it('shows the form if template names request failed', () => { - mock - .onGet('/issuable-templates-path') - .reply(() => Promise.reject(new Error('something went wrong'))); + wrapper.setProps({ issuableType: 'merge request' }); - return wrapper.vm.requestTemplatesAndShowForm().then(() => { - expect(createAlert).toHaveBeenCalledWith({ message: 'Error updating issue' }); + await updateIssuable(); - expect(formSpy).toHaveBeenCalledWith(); + expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); + expect(createAlert).toHaveBeenCalledWith({ + message: `Error updating merge request. Request failed with status code 401`, + }); }); - }); - }); - - describe('show inline edit button', () => { - it('should render by default', () => { - expect(findTitle().props('showInlineEditButton')).toBe(true); - }); - - it('should render if showInlineEditButton', async () => { - wrapper.setProps({ showInlineEditButton: true }); - - await nextTick(); - expect(findTitle().props('showInlineEditButton')).toBe(true); - }); - it('should not render if showInlineEditButton is false', async () => { - wrapper.setProps({ showInlineEditButton: false }); - - await nextTick(); - expect(findTitle().props('showInlineEditButton')).toBe(false); - }); - }); - - describe('updateStoreState', () => { - it('should make a request and update the state of the store', () => { - const data = { foo: 1 }; - const getDataSpy = jest.spyOn(wrapper.vm.service, 'getData').mockResolvedValue({ data }); - const updateStateSpy = jest - .spyOn(wrapper.vm.store, 'updateState') - .mockImplementation(jest.fn); - - return wrapper.vm.updateStoreState().then(() => { - expect(getDataSpy).toHaveBeenCalled(); - expect(updateStateSpy).toHaveBeenCalledWith(data); - }); - }); + it('shows error message from backend if exists', async () => { + const msg = 'Custom error message from backend'; + axiosMock.onPut().reply(HTTP_STATUS_UNAUTHORIZED, { errors: [msg] }); - it('should show error message if store update fails', () => { - jest.spyOn(wrapper.vm.service, 'getData').mockRejectedValue(); - wrapper.setProps({ issuableType: 'merge request' }); + await updateIssuable(); - return wrapper.vm.updateStoreState().then(() => { expect(createAlert).toHaveBeenCalledWith({ - message: `Error updating ${wrapper.vm.issuableType}`, + message: `Error updating issue. ${msg}`, }); }); }); }); - describe('issueChanged', () => { - beforeEach(() => { - wrapper.vm.store.formState.title = ''; - wrapper.vm.store.formState.description = ''; - wrapper.setProps({ - initialDescriptionText: '', - initialTitleText: '', - }); - }); - - it('returns true when title is changed', () => { - wrapper.vm.store.formState.title = 'RandomText'; - - expect(wrapper.vm.issueChanged).toBe(true); - }); - - it('returns false when title is empty null', () => { - wrapper.vm.store.formState.title = null; - - expect(wrapper.vm.issueChanged).toBe(false); - }); - - it('returns true when description is changed', () => { - wrapper.vm.store.formState.description = 'RandomText'; - - expect(wrapper.vm.issueChanged).toBe(true); + describe('Locked warning', () => { + beforeEach(async () => { + await createComponent(); }); - it('returns false when description is empty null', () => { - wrapper.vm.store.formState.description = null; - - expect(wrapper.vm.issueChanged).toBe(false); - }); - - it('returns false when `initialDescriptionText` is null and `formState.description` is empty string', () => { - wrapper.vm.store.formState.description = ''; - wrapper.setProps({ initialDescriptionText: null }); + it('shows locked warning if form is open & data is different', async () => { + await openForm(); + await advanceToNextPoll(); - expect(wrapper.vm.issueChanged).toBe(false); + expect(findForm().props().formState.lockedWarningVisible).toBe(true); + expect(findForm().props().formState.lock_version).toBe(1); }); }); describe('sticky header', () => { + beforeEach(async () => { + await createComponent(); + }); + describe('when title is in view', () => { it('is not shown', () => { expect(findStickyHeader().exists()).toBe(false); @@ -467,21 +348,18 @@ describe('Issuable output', () => { }); describe('when title is not in view', () => { - beforeEach(() => { - wrapper.vm.state.titleText = 'Sticky header title'; + beforeEach(async () => { wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear'); }); it('shows with title', () => { - expect(findStickyHeader().text()).toContain('Sticky header title'); + expect(findStickyHeader().text()).toContain(initialRequest.title_text); }); it('shows with title for an epic', async () => { - wrapper.setProps({ issuableType: 'epic' }); - - await nextTick(); + await wrapper.setProps({ issuableType: 'epic' }); - expect(findStickyHeader().text()).toContain('Sticky header title'); + expect(findStickyHeader().text()).toContain(' this is a title'); }); it.each` @@ -493,9 +371,7 @@ describe('Issuable output', () => { `( 'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus', async ({ issuableType, issuableStatus, statusIcon }) => { - wrapper.setProps({ issuableType, issuableStatus }); - - await nextTick(); + await wrapper.setProps({ issuableType, issuableStatus }); expect(findStickyHeader().findComponent(GlIcon).props('name')).toBe(statusIcon); }, @@ -507,9 +383,7 @@ describe('Issuable output', () => { ${'shows with Closed when status is closed'} | ${STATUS_CLOSED} ${'shows with Open when status is reopened'} | ${STATUS_REOPENED} `('$title', async ({ state }) => { - wrapper.setProps({ issuableStatus: state }); - - await nextTick(); + await wrapper.setProps({ issuableStatus: state }); expect(findStickyHeader().text()).toContain(IssuableStatusText[state]); }); @@ -519,9 +393,7 @@ describe('Issuable output', () => { ${'does not show confidential badge when issue is not confidential'} | ${false} ${'shows confidential badge when issue is confidential'} | ${true} `('$title', async ({ isConfidential }) => { - wrapper.setProps({ isConfidential }); - - await nextTick(); + await wrapper.setProps({ isConfidential }); const confidentialEl = findConfidentialBadge(); expect(confidentialEl.exists()).toBe(isConfidential); @@ -538,9 +410,7 @@ describe('Issuable output', () => { ${'does not show locked badge when issue is not locked'} | ${false} ${'shows locked badge when issue is locked'} | ${true} `('$title', async ({ isLocked }) => { - wrapper.setProps({ isLocked }); - - await nextTick(); + await wrapper.setProps({ isLocked }); expect(findLockedBadge().exists()).toBe(isLocked); }); @@ -550,9 +420,7 @@ describe('Issuable output', () => { ${'does not show hidden badge when issue is not hidden'} | ${false} ${'shows hidden badge when issue is hidden'} | ${true} `('$title', async ({ isHidden }) => { - wrapper.setProps({ isHidden }); - - await nextTick(); + await wrapper.setProps({ isHidden }); const hiddenBadge = findHiddenBadge(); @@ -569,6 +437,10 @@ describe('Issuable output', () => { }); describe('Composable description component', () => { + beforeEach(async () => { + await createComponent(); + }); + const findIncidentTabs = () => wrapper.findComponent(IncidentTabs); const borderClass = 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6'; @@ -587,13 +459,13 @@ describe('Issuable output', () => { }); describe('when using incident tabs description wrapper', () => { - beforeEach(() => { - mountComponent( - { + beforeEach(async () => { + await createComponent({ + props: { descriptionComponent: IncidentTabs, showTitleBorder: false, }, - { + options: { mocks: { $apollo: { queries: { @@ -604,7 +476,7 @@ describe('Issuable output', () => { }, }, }, - ); + }); }); it('does not the description component', () => { @@ -622,48 +494,77 @@ describe('Issuable output', () => { }); describe('taskListUpdateStarted', () => { - it('stops polling', () => { - jest.spyOn(wrapper.vm.poll, 'stop'); + beforeEach(async () => { + await createComponent(); + }); + + it('stops polling', async () => { + expect(findTitle().props().titleText).toBe(initialRequest.title_text); + + findDescription().vm.$emit('taskListUpdateStarted'); - wrapper.vm.taskListUpdateStarted(); + await advanceToNextPoll(); - expect(wrapper.vm.poll.stop).toHaveBeenCalled(); + expect(findTitle().props().titleText).toBe(initialRequest.title_text); }); }); describe('taskListUpdateSucceeded', () => { - it('enables polling', () => { - jest.spyOn(wrapper.vm.poll, 'enable'); - jest.spyOn(wrapper.vm.poll, 'makeDelayedRequest'); + beforeEach(async () => { + await createComponent(); + findDescription().vm.$emit('taskListUpdateStarted'); + }); + + it('enables polling', async () => { + // Ensure that polling is not working before + expect(findTitle().props().titleText).toBe(initialRequest.title_text); + await advanceToNextPoll(); + + expect(findTitle().props().titleText).toBe(initialRequest.title_text); - wrapper.vm.taskListUpdateSucceeded(); + // Enable Polling an move forward + findDescription().vm.$emit('taskListUpdateSucceeded'); + await advanceToNextPoll(); - expect(wrapper.vm.poll.enable).toHaveBeenCalled(); - expect(wrapper.vm.poll.makeDelayedRequest).toHaveBeenCalledWith(POLLING_DELAY); + // Title has changed: polling works! + expect(findTitle().props().titleText).toBe(secondRequest.title_text); }); }); describe('taskListUpdateFailed', () => { - it('enables polling and calls updateStoreState', () => { - jest.spyOn(wrapper.vm.poll, 'enable'); - jest.spyOn(wrapper.vm.poll, 'makeDelayedRequest'); - jest.spyOn(wrapper.vm, 'updateStoreState'); + beforeEach(async () => { + await createComponent(); + findDescription().vm.$emit('taskListUpdateStarted'); + }); + + it('enables polling and calls updateStoreState', async () => { + // Ensure that polling is not working before + expect(findTitle().props().titleText).toBe(initialRequest.title_text); + await advanceToNextPoll(); - wrapper.vm.taskListUpdateFailed(); + expect(findTitle().props().titleText).toBe(initialRequest.title_text); - expect(wrapper.vm.poll.enable).toHaveBeenCalled(); - expect(wrapper.vm.poll.makeDelayedRequest).toHaveBeenCalledWith(POLLING_DELAY); - expect(wrapper.vm.updateStoreState).toHaveBeenCalled(); + // Enable Polling an move forward + findDescription().vm.$emit('taskListUpdateFailed'); + await advanceToNextPoll(); + + // Title has changed: polling works! + expect(findTitle().props().titleText).toBe(secondRequest.title_text); }); }); describe('saveDescription event', () => { + beforeEach(async () => { + await createComponent(); + }); + it('makes request to update issue', async () => { const description = 'I have been updated!'; findDescription().vm.$emit('saveDescription', description); + await waitForPromises(); - expect(mock.history.put[0].data).toContain(description); + expect(axiosMock.history.put[0].data).toContain(description); }); }); }); diff --git a/spec/frontend/issues/show/components/delete_issue_modal_spec.js b/spec/frontend/issues/show/components/delete_issue_modal_spec.js index 97a091a1748..b8adeb24005 100644 --- a/spec/frontend/issues/show/components/delete_issue_modal_spec.js +++ b/spec/frontend/issues/show/components/delete_issue_modal_spec.js @@ -20,10 +20,6 @@ describe('DeleteIssueModal component', () => { const mountComponent = (props = {}) => shallowMount(DeleteIssueModal, { propsData: { ...defaultProps, ...props } }); - afterEach(() => { - wrapper.destroy(); - }); - describe('modal', () => { it('renders', () => { wrapper = mountComponent(); diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js index da51372dd3d..740b2f782e4 100644 --- a/spec/frontend/issues/show/components/description_spec.js +++ b/spec/frontend/issues/show/components/description_spec.js @@ -1,25 +1,19 @@ import $ from 'jquery'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; -import { GlModal } from '@gitlab/ui'; import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql'; import setWindowLocation from 'helpers/set_window_location_helper'; -import { stubComponent } from 'helpers/stub_component'; import { TEST_HOST } from 'helpers/test_constants'; -import { mockTracking } from 'helpers/tracking_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import Description from '~/issues/show/components/description.vue'; import eventHub from '~/issues/show/event_hub'; -import { updateHistory } from '~/lib/utils/url_utility'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql'; import workItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql'; import TaskList from '~/task_list'; -import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; -import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; import { renderGFM } from '~/behaviors/markdown/render_gfm'; import { createWorkItemMutationErrorResponse, @@ -27,14 +21,9 @@ import { getIssueDetailsResponse, projectWorkItemTypesQueryResponse, } from 'jest/work_items/mock_data'; -import { - descriptionProps as initialProps, - descriptionHtmlWithList, - descriptionHtmlWithCheckboxes, - descriptionHtmlWithTask, -} from '../mock_data/mock_data'; +import { descriptionProps as initialProps, descriptionHtmlWithList } from '../mock_data/mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility', () => ({ ...jest.requireActual('~/lib/utils/url_utility'), updateHistory: jest.fn(), @@ -43,9 +32,6 @@ jest.mock('~/task_list'); jest.mock('~/behaviors/markdown/render_gfm'); const mockSpriteIcons = '/icons.svg'; -const showModal = jest.fn(); -const hideModal = jest.fn(); -const showDetailsModal = jest.fn(); const $toast = { show: jest.fn(), }; @@ -62,7 +48,6 @@ const workItemTypesQueryHandler = jest.fn().mockResolvedValue(projectWorkItemTyp describe('Description component', () => { let wrapper; - let originalGon; Vue.use(VueApollo); @@ -70,21 +55,16 @@ describe('Description component', () => { const findTextarea = () => wrapper.find('[data-testid="textarea"]'); const findListItems = () => findGfmContent().findAll('ul > li'); const findTaskActionButtons = () => wrapper.findAll('.task-list-item-actions'); - const findTaskLink = () => wrapper.find('a.gfm-issue'); - const findModal = () => wrapper.findComponent(GlModal); - const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal); function createComponent({ props = {}, provide, issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse), createWorkItemMutationHandler, - ...options } = {}) { wrapper = shallowMountExtended(Description, { propsData: { issueId: 1, - issueIid: 1, ...initialProps, ...props, }, @@ -102,25 +82,10 @@ describe('Description component', () => { mocks: { $toast, }, - stubs: { - GlModal: stubComponent(GlModal, { - methods: { - show: showModal, - hide: hideModal, - }, - }), - WorkItemDetailModal: stubComponent(WorkItemDetailModal, { - methods: { - show: showDetailsModal, - }, - }), - }, - ...options, }); } beforeEach(() => { - originalGon = window.gon; window.gon = { sprite_icons: mockSpriteIcons }; setWindowLocation(TEST_HOST); @@ -136,8 +101,6 @@ describe('Description component', () => { }); afterAll(() => { - window.gon = originalGon; - $('.issuable-meta .flash-container').remove(); }); @@ -285,7 +248,6 @@ describe('Description component', () => { props: { descriptionHtml: descriptionHtmlWithList, }, - attachTo: document.body, }); await nextTick(); }); @@ -325,33 +287,6 @@ describe('Description component', () => { }); }); - describe('description with checkboxes', () => { - beforeEach(() => { - createComponent({ - props: { - descriptionHtml: descriptionHtmlWithCheckboxes, - }, - }); - return nextTick(); - }); - - it('renders a list of hidden buttons corresponding to checkboxes in description HTML', () => { - expect(findTaskActionButtons()).toHaveLength(3); - }); - - it('does not show a modal by default', () => { - expect(findModal().exists()).toBe(false); - }); - - it('shows toast after delete success', async () => { - const newDesc = 'description'; - findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc); - - expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]); - expect($toast.show).toHaveBeenCalledWith('Task deleted'); - }); - }); - describe('task list item actions', () => { describe('converting the task list item to a task', () => { describe('when successful', () => { @@ -391,11 +326,7 @@ describe('Description component', () => { }); it('calls a mutation to create a task', () => { - const { - confidential, - iteration, - milestone, - } = issueDetailsResponse.data.workspace.issuable; + const { confidential, iteration, milestone } = issueDetailsResponse.data.issue; expect(createWorkItemMutationHandler).toHaveBeenCalledWith({ input: { confidential, @@ -468,109 +399,4 @@ describe('Description component', () => { }); }); }); - - describe('work items detail', () => { - describe('when opening and closing', () => { - beforeEach(() => { - createComponent({ - props: { - descriptionHtml: descriptionHtmlWithTask, - }, - }); - return nextTick(); - }); - - it('opens when task button is clicked', async () => { - await findTaskLink().trigger('click'); - - expect(showDetailsModal).toHaveBeenCalled(); - expect(updateHistory).toHaveBeenCalledWith({ - url: `${TEST_HOST}/?work_item_id=2`, - replace: true, - }); - }); - - it('closes from an open state', async () => { - await findTaskLink().trigger('click'); - - findWorkItemDetailModal().vm.$emit('close'); - await nextTick(); - - expect(updateHistory).toHaveBeenLastCalledWith({ - url: `${TEST_HOST}/`, - replace: true, - }); - }); - - it('tracks when opened', async () => { - const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - - await findTaskLink().trigger('click'); - - expect(trackingSpy).toHaveBeenCalledWith( - TRACKING_CATEGORY_SHOW, - 'viewed_work_item_from_modal', - { - category: TRACKING_CATEGORY_SHOW, - label: 'work_item_view', - property: 'type_task', - }, - ); - }); - }); - - describe('when url query `work_item_id` exists', () => { - it.each` - behavior | workItemId | modalOpened - ${'opens'} | ${'2'} | ${1} - ${'does not open'} | ${'123'} | ${0} - ${'does not open'} | ${'123e'} | ${0} - ${'does not open'} | ${'12e3'} | ${0} - ${'does not open'} | ${'1e23'} | ${0} - ${'does not open'} | ${'x'} | ${0} - ${'does not open'} | ${'undefined'} | ${0} - `( - '$behavior when url contains `work_item_id=$workItemId`', - async ({ workItemId, modalOpened }) => { - setWindowLocation(`?work_item_id=${workItemId}`); - - createComponent({ - props: { descriptionHtml: descriptionHtmlWithTask }, - }); - - expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened); - }, - ); - }); - }); - - describe('when hovering task links', () => { - beforeEach(() => { - createComponent({ - props: { - descriptionHtml: descriptionHtmlWithTask, - }, - }); - return nextTick(); - }); - - it('prefetches work item detail after work item link is hovered for 150ms', async () => { - await findTaskLink().trigger('mouseover'); - jest.advanceTimersByTime(150); - await waitForPromises(); - - expect(queryHandler).toHaveBeenCalledWith({ - id: 'gid://gitlab/WorkItem/2', - }); - }); - - it('does not work item detail after work item link is hovered for less than 150ms', async () => { - await findTaskLink().trigger('mouseover'); - await findTaskLink().trigger('mouseout'); - jest.advanceTimersByTime(150); - await waitForPromises(); - - expect(queryHandler).not.toHaveBeenCalled(); - }); - }); }); diff --git a/spec/frontend/issues/show/components/edit_actions_spec.js b/spec/frontend/issues/show/components/edit_actions_spec.js index 11c43ea4388..ca561149806 100644 --- a/spec/frontend/issues/show/components/edit_actions_spec.js +++ b/spec/frontend/issues/show/components/edit_actions_spec.js @@ -56,10 +56,6 @@ describe('Edit Actions component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders all buttons as enabled', () => { const buttons = findEditButtons().wrappers; buttons.forEach((button) => { diff --git a/spec/frontend/issues/show/components/edited_spec.js b/spec/frontend/issues/show/components/edited_spec.js index aa6e0a9dceb..a509627c347 100644 --- a/spec/frontend/issues/show/components/edited_spec.js +++ b/spec/frontend/issues/show/components/edited_spec.js @@ -15,10 +15,6 @@ describe('Edited component', () => { const mountComponent = (propsData) => mount(Edited, { propsData }); const updatedAt = '2017-05-15T12:31:04.428Z'; - afterEach(() => { - wrapper.destroy(); - }); - it('renders an edited at+by string', () => { wrapper = mountComponent({ updatedAt, diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js index 273ddfdd5d4..5c145ed4707 100644 --- a/spec/frontend/issues/show/components/fields/description_spec.js +++ b/spec/frontend/issues/show/components/fields/description_spec.js @@ -33,11 +33,6 @@ describe('Description field component', () => { jest.spyOn(eventHub, '$emit'); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders markdown field with description', () => { wrapper = mountComponent(); @@ -80,17 +75,18 @@ describe('Description field component', () => { }); it('uses the MarkdownEditor component to edit markdown', () => { - expect(findMarkdownEditor().props()).toEqual( - expect.objectContaining({ - value: 'test', - renderMarkdownPath: '/', - markdownDocsPath: '/', - quickActionsDocsPath: expect.any(String), - autofocus: true, - supportsQuickActions: true, - enableAutocomplete: true, - }), - ); + expect(findMarkdownEditor().props()).toMatchObject({ + value: 'test', + renderMarkdownPath: '/', + autofocus: true, + supportsQuickActions: true, + quickActionsDocsPath: expect.any(String), + }); + + expect(findMarkdownEditor().vm.$attrs).toMatchObject({ + 'enable-autocomplete': true, + 'markdown-docs-path': '/', + }); }); it('triggers update with meta+enter', () => { diff --git a/spec/frontend/issues/show/components/fields/description_template_spec.js b/spec/frontend/issues/show/components/fields/description_template_spec.js index 79a3bfa9840..1e8d5e2dd95 100644 --- a/spec/frontend/issues/show/components/fields/description_template_spec.js +++ b/spec/frontend/issues/show/components/fields/description_template_spec.js @@ -22,10 +22,6 @@ describe('Issue description template component with templates as hash', () => { wrapper = shallowMount(descriptionTemplate, options); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders templates as JSON hash in data attribute', () => { createComponent(); expect(findIssuableSelector().attributes('data-data')).toBe( diff --git a/spec/frontend/issues/show/components/fields/title_spec.js b/spec/frontend/issues/show/components/fields/title_spec.js index a5fa96d8d64..b28762f1520 100644 --- a/spec/frontend/issues/show/components/fields/title_spec.js +++ b/spec/frontend/issues/show/components/fields/title_spec.js @@ -17,11 +17,6 @@ describe('Title field component', () => { }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders form control with formState title', () => { expect(findInput().element.value).toBe('test'); }); diff --git a/spec/frontend/issues/show/components/fields/type_spec.js b/spec/frontend/issues/show/components/fields/type_spec.js index 27ac0e1baf3..e655cf3b37d 100644 --- a/spec/frontend/issues/show/components/fields/type_spec.js +++ b/spec/frontend/issues/show/components/fields/type_spec.js @@ -1,4 +1,4 @@ -import { GlFormGroup, GlListbox, GlIcon } from '@gitlab/ui'; +import { GlFormGroup, GlCollapsibleListbox, GlIcon } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; @@ -32,7 +32,7 @@ describe('Issue type field component', () => { }, }; - const findListBox = () => wrapper.findComponent(GlListbox); + const findListBox = () => wrapper.findComponent(GlCollapsibleListbox); const findFormGroup = () => wrapper.findComponent(GlFormGroup); const findAllIssueItems = () => wrapper.findAll('[data-testid="issue-type-list-item"]'); const findIssueItemAt = (at) => findAllIssueItems().at(at); @@ -60,10 +60,6 @@ describe('Issue type field component', () => { mockIssueStateData = jest.fn(); }); - afterEach(() => { - wrapper.destroy(); - }); - it.each` at | text | icon ${0} | ${issuableTypes[0].text} | ${issuableTypes[0].icon} diff --git a/spec/frontend/issues/show/components/form_spec.js b/spec/frontend/issues/show/components/form_spec.js index aedb974cbd0..b8ed33801f2 100644 --- a/spec/frontend/issues/show/components/form_spec.js +++ b/spec/frontend/issues/show/components/form_spec.js @@ -30,10 +30,6 @@ describe('Inline edit form component', () => { projectNamespace: '/', }; - afterEach(() => { - wrapper.destroy(); - }); - const createComponent = (props) => { wrapper = shallowMount(formComponent, { propsData: { diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js index 3d9dad3a721..58ec7387851 100644 --- a/spec/frontend/issues/show/components/header_actions_spec.js +++ b/spec/frontend/issues/show/components/header_actions_spec.js @@ -1,20 +1,22 @@ import Vue, { nextTick } from 'vue'; -import { GlButton, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui'; +import { GlDropdownItem, GlLink, GlModal, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vuex from 'vuex'; import { mockTracking } from 'helpers/tracking_helper'; -import { createAlert, VARIANT_SUCCESS } from '~/flash'; -import { IssueType, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; +import { STATUS_CLOSED, STATUS_OPEN, TYPE_INCIDENT, TYPE_ISSUE } 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 issuesEventHub from '~/issues/show/event_hub'; import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutation.graphql'; import * as urlUtility from '~/lib/utils/url_utility'; import eventHub from '~/notes/event_hub'; import createStore from '~/notes/stores'; -jest.mock('~/flash'); +jest.mock('~/alert'); +jest.mock('~/issues/show/event_hub', () => ({ $emit: jest.fn() })); describe('HeaderActions component', () => { let dispatchEventSpy; @@ -36,7 +38,7 @@ describe('HeaderActions component', () => { iid: '32', isIssueAuthor: true, issuePath: 'gitlab-org/gitlab-test/-/issues/1', - issueType: IssueType.Issue, + issueType: TYPE_ISSUE, newIssuePath: 'gitlab-org/gitlab-test/-/issues/new', projectPath: 'gitlab-org/gitlab-test', reportAbusePath: '-/abuse_reports/add_category', @@ -67,7 +69,8 @@ describe('HeaderActions component', () => { }, }; - const findToggleIssueStateButton = () => wrapper.findComponent(GlButton); + const findToggleIssueStateButton = () => wrapper.find(`[data-testid="toggle-button"]`); + const findEditButton = () => wrapper.find(`[data-testid="edit-button"]`); const findDropdownBy = (dataTestId) => wrapper.find(`[data-testid="${dataTestId}"]`); const findMobileDropdown = () => findDropdownBy('mobile-dropdown'); @@ -103,6 +106,9 @@ describe('HeaderActions component', () => { mutate: mutateMock, }, }, + stubs: { + GlButton, + }, }); }; @@ -113,13 +119,12 @@ describe('HeaderActions component', () => { if (visitUrlSpy) { visitUrlSpy.mockRestore(); } - wrapper.destroy(); }); describe.each` issueType - ${IssueType.Issue} - ${IssueType.Incident} + ${TYPE_ISSUE} + ${TYPE_INCIDENT} `('when issue type is $issueType', ({ issueType }) => { describe('close/reopen button', () => { describe.each` @@ -240,6 +245,30 @@ describe('HeaderActions component', () => { }); }); }); + + describe(`show edit button ${issueType}`, () => { + beforeEach(() => { + wrapper = mountComponent({ + props: { + canUpdateIssue: true, + canCreateIssue: false, + isIssueAuthor: true, + issueType, + canReportSpam: false, + canPromoteToEpic: false, + }, + }); + }); + it(`shows the edit button`, () => { + expect(findEditButton().exists()).toBe(true); + }); + + it('should trigger "open.form" event when clicked', async () => { + expect(issuesEventHub.$emit).not.toHaveBeenCalled(); + await findEditButton().trigger('click'); + expect(issuesEventHub.$emit).toHaveBeenCalledWith('open.form'); + }); + }); }); describe('delete issue button', () => { 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 6c923cae0cc..6b68e7a0da6 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 @@ -9,7 +9,7 @@ import createTimelineEventMutation from '~/issues/show/components/incidents/grap import getTimelineEvents from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql'; import { timelineFormI18n } from '~/issues/show/components/incidents/constants'; import createMockApollo from 'helpers/mock_apollo_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { useFakeDate } from 'helpers/fake_date'; import { timelineEventsCreateEventResponse, @@ -19,7 +19,7 @@ import { Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); const fakeDate = '2020-07-08T00:00:00.000Z'; @@ -99,7 +99,6 @@ describe('Create Timeline events', () => { afterEach(() => { createAlert.mockReset(); - wrapper.destroy(); }); describe('createIncidentTimelineEvent', () => { diff --git a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js index 33a3a6eddfc..0f4fb02a40b 100644 --- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js +++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js @@ -1,4 +1,5 @@ import merge from 'lodash/merge'; +import { nextTick } from 'vue'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { trackIncidentDetailsViewsOptions } from '~/incidents/constants'; import DescriptionComponent from '~/issues/show/components/description.vue'; @@ -11,6 +12,11 @@ import Tracking from '~/tracking'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; import { descriptionProps } from '../../mock_data/mock_data'; +const push = jest.fn(); +const $router = { + push, +}; + const mockAlert = { __typename: 'AlertManagementAlert', detailsUrl: INVALID_URL, @@ -28,12 +34,20 @@ const defaultMocks = { }, }, }, + $route: { params: {} }, + $router, }; describe('Incident Tabs component', () => { let wrapper; - const mountComponent = ({ data = {}, options = {}, mount = shallowMountExtended } = {}) => { + const mountComponent = ({ + data = {}, + options = {}, + mount = shallowMountExtended, + hasLinkedAlerts = false, + mocks = {}, + } = {}) => { wrapper = mount( IncidentTabs, merge( @@ -54,11 +68,12 @@ describe('Incident Tabs component', () => { slaFeatureAvailable: true, canUpdate: true, canUpdateTimelineEvent: true, + hasLinkedAlerts, }, data() { return { alert: mockAlert, ...data }; }, - mocks: defaultMocks, + mocks: { ...defaultMocks, ...mocks }, }, options, ), @@ -102,11 +117,13 @@ describe('Incident Tabs component', () => { }); it('renders the alert details tab', () => { + mountComponent({ hasLinkedAlerts: true }); expect(findAlertDetailsTab().exists()).toBe(true); expect(findAlertDetailsTab().attributes('title')).toBe('Alert details'); }); it('renders the alert details table with the correct props', () => { + mountComponent({ hasLinkedAlerts: true }); const alert = { iid: mockAlert.iid }; expect(findAlertDetailsComponent().props('alert')).toMatchObject(alert); @@ -156,6 +173,40 @@ describe('Incident Tabs component', () => { expect(findActiveTabs()).toHaveLength(1); expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.timelineTitle); + expect(push).toHaveBeenCalledWith('/timeline'); + }); + }); + + describe('loading page with tab', () => { + it('shows the timeline tab when timeline path is passed', async () => { + mountComponent({ + mount: mountExtended, + mocks: { $route: { params: { tabId: 'timeline' } } }, + }); + await nextTick(); + expect(findActiveTabs()).toHaveLength(1); + expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.timelineTitle); + }); + + it('shows the alerts tab when timeline path is passed', async () => { + mountComponent({ + mount: mountExtended, + mocks: { $route: { params: { tabId: 'alerts' } } }, + hasLinkedAlerts: true, + }); + await nextTick(); + expect(findActiveTabs()).toHaveLength(1); + expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.alertsTitle); + }); + + it('shows the metrics tab when metrics path is passed', async () => { + mountComponent({ + mount: mountExtended, + mocks: { $route: { params: { tabId: 'metrics' } } }, + }); + await nextTick(); + expect(findActiveTabs()).toHaveLength(1); + expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.metricsTitle); }); }); }); 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 e352f9708e4..af01fd34336 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 @@ -10,12 +10,12 @@ import { TIMELINE_EVENT_TAGS, timelineEventTagsI18n, } from '~/issues/show/components/incidents/constants'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { useFakeDate } from 'helpers/fake_date'; Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); const fakeDate = '2020-07-08T00:00:00.000Z'; @@ -51,7 +51,6 @@ describe('Timeline events form', () => { afterEach(() => { createAlert.mockReset(); - wrapper.destroy(); }); const findMarkdownField = () => wrapper.findComponent(MarkdownField); diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js index 26fda877089..8d79dece888 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js @@ -11,7 +11,7 @@ import deleteTimelineEventMutation from '~/issues/show/components/incidents/grap import editTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/edit_timeline_event.mutation.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; import { useFakeDate } from 'helpers/fake_date'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { mockEvents, timelineEventsDeleteEventResponse, @@ -26,7 +26,7 @@ import { Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); const mockConfirmAction = ({ confirmed }) => { @@ -77,10 +77,6 @@ describe('IncidentTimelineEventList', () => { mountComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { it('groups items correctly', () => { expect(findTimelineEventGroups()).toHaveLength(2); diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js index 63474070701..48c3f0984a0 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js @@ -8,13 +8,13 @@ import IncidentTimelineEventsList from '~/issues/show/components/incidents/timel import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue'; import timelineEventsQuery from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { timelineTabI18n } from '~/issues/show/components/incidents/constants'; import { timelineEventsQueryListResponse, timelineEventsQueryEmptyResponse } from './mock_data'; Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); const graphQLError = new Error('GraphQL error'); const listResponse = jest.fn().mockResolvedValue(timelineEventsQueryListResponse); diff --git a/spec/frontend/issues/show/components/incidents/utils_spec.js b/spec/frontend/issues/show/components/incidents/utils_spec.js index 75be17f9889..8ee0d906dd4 100644 --- a/spec/frontend/issues/show/components/incidents/utils_spec.js +++ b/spec/frontend/issues/show/components/incidents/utils_spec.js @@ -5,10 +5,10 @@ import { getUtcShiftedDate, getPreviousEventTags, } from '~/issues/show/components/incidents/utils'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { mockTimelineEventTags } from './mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('incident utils', () => { describe('display and log error', () => { diff --git a/spec/frontend/issues/show/components/locked_warning_spec.js b/spec/frontend/issues/show/components/locked_warning_spec.js index dd3c7c58380..f8a8c999632 100644 --- a/spec/frontend/issues/show/components/locked_warning_spec.js +++ b/spec/frontend/issues/show/components/locked_warning_spec.js @@ -13,11 +13,6 @@ describe('LockedWarning component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findAlert = () => wrapper.findComponent(GlAlert); const findLink = () => wrapper.findComponent(GlLink); diff --git a/spec/frontend/issues/show/components/task_list_item_actions_spec.js b/spec/frontend/issues/show/components/task_list_item_actions_spec.js index d52f9d57453..8caa5236796 100644 --- a/spec/frontend/issues/show/components/task_list_item_actions_spec.js +++ b/spec/frontend/issues/show/components/task_list_item_actions_spec.js @@ -17,7 +17,7 @@ describe('TaskListItemActions component', () => { document.body.appendChild(li); wrapper = shallowMount(TaskListItemActions, { - provide: { canUpdate: true, toggleClass: 'task-list-item-actions' }, + provide: { canUpdate: true }, attachTo: document.querySelector('div'), }); }; diff --git a/spec/frontend/issues/show/components/title_spec.js b/spec/frontend/issues/show/components/title_spec.js index 7560b733ae6..16ac675e12c 100644 --- a/spec/frontend/issues/show/components/title_spec.js +++ b/spec/frontend/issues/show/components/title_spec.js @@ -1,96 +1,59 @@ -import Vue, { nextTick } from 'vue'; +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import titleComponent from '~/issues/show/components/title.vue'; -import eventHub from '~/issues/show/event_hub'; -import Store from '~/issues/show/stores'; +import Title from '~/issues/show/components/title.vue'; describe('Title component', () => { - let vm; - beforeEach(() => { + let wrapper; + + const getTitleHeader = () => wrapper.findByTestId('issue-title'); + + const createWrapper = (props) => { setHTMLFixture(`<title />`); - const Component = Vue.extend(titleComponent); - const store = new Store({ - titleHtml: '', - descriptionHtml: '', - issuableRef: '', - }); - vm = new Component({ + wrapper = shallowMountExtended(Title, { propsData: { issuableRef: '#1', titleHtml: 'Testing <img />', titleText: 'Testing', - showForm: false, - formState: store.formState, + ...props, }, - }).$mount(); - }); + }); + }; afterEach(() => { resetHTMLFixture(); }); it('renders title HTML', () => { - expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>'); - }); - - it('updates page title when changing titleHtml', async () => { - const spy = jest.spyOn(vm, 'setPageTitle'); - vm.titleHtml = 'test'; + createWrapper(); - await nextTick(); - expect(spy).toHaveBeenCalled(); + expect(getTitleHeader().element.innerHTML.trim()).toBe('Testing <img>'); }); it('animates title changes', async () => { - vm.titleHtml = 'test'; + createWrapper(); - await nextTick(); + await wrapper.setProps({ + titleHtml: 'test', + }); - expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-pre-pulse'); - jest.runAllTimers(); + expect(getTitleHeader().classes('issue-realtime-pre-pulse')).toBe(true); + jest.runAllTimers(); await nextTick(); - expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-trigger-pulse'); + expect(getTitleHeader().classes('issue-realtime-trigger-pulse')).toBe(true); }); it('updates page title after changing title', async () => { - vm.titleHtml = 'changed'; - vm.titleText = 'changed'; - - await nextTick(); - expect(document.querySelector('title').textContent.trim()).toContain('changed'); - }); + createWrapper(); - describe('inline edit button', () => { - it('should not show by default', () => { - expect(vm.$el.querySelector('.btn-edit')).toBeNull(); + await wrapper.setProps({ + titleHtml: 'changed', + titleText: 'changed', }); - it('should not show if canUpdate is false', () => { - vm.showInlineEditButton = true; - vm.canUpdate = false; - - expect(vm.$el.querySelector('.btn-edit')).toBeNull(); - }); - - it('should show if showInlineEditButton and canUpdate', () => { - vm.showInlineEditButton = true; - vm.canUpdate = true; - - expect(vm.$el.querySelector('.btn-edit')).toBeDefined(); - }); - - it('should trigger open.form event when clicked', async () => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - vm.showInlineEditButton = true; - vm.canUpdate = true; - - await nextTick(); - vm.$el.querySelector('.btn-edit').click(); - - expect(eventHub.$emit).toHaveBeenCalledWith('open.form'); - }); + expect(document.querySelector('title').textContent.trim()).toContain('changed'); }); }); diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js index 9f0b6fb1148..86d09665947 100644 --- a/spec/frontend/issues/show/mock_data/mock_data.js +++ b/spec/frontend/issues/show/mock_data/mock_data.js @@ -24,6 +24,19 @@ export const secondRequest = { lock_version: 2, }; +export const putRequest = { + web_url: window.location.pathname, + title: '<p>PUT</p>', + title_text: 'PUT', + description: '<p>PUT_DESC</p>', + description_text: 'PUT_DESC', + task_status: '0 of 0 completed', + updated_at: '2016-05-15T12:31:04.428Z', + updated_by_name: 'Other User', + updated_by_path: '/other_user', + lock_version: 2, +}; + export const descriptionProps = { canUpdate: true, descriptionHtml: 'test', @@ -66,47 +79,3 @@ export const descriptionHtmlWithList = ` <li data-sourcepos="3:1-3:8">todo 3</li> </ul> `; - -export const descriptionHtmlWithCheckboxes = ` - <ul dir="auto" class="task-list" data-sourcepos"3:1-5:12"> - <li class="task-list-item" data-sourcepos="3:1-3:11"> - <input class="task-list-item-checkbox" type="checkbox"> todo 1 - </li> - <li class="task-list-item" data-sourcepos="4:1-4:12"> - <input class="task-list-item-checkbox" type="checkbox"> todo 2 - </li> - <li class="task-list-item" data-sourcepos="5:1-5:12"> - <input class="task-list-item-checkbox" type="checkbox"> todo 3 - </li> - </ul> -`; - -export const descriptionHtmlWithTask = ` - <ul data-sourcepos="1:1-3:7" class="task-list" dir="auto"> - <li data-sourcepos="1:1-1:10" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled> - <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="task">1 (#48)</a> - </li> - <li data-sourcepos="2:1-2:7" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled> 2 - </li> - <li data-sourcepos="3:1-3:7" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled> 3 - </li> - </ul> -`; - -export const descriptionHtmlWithIssue = ` - <ul data-sourcepos="1:1-3:7" class="task-list" dir="auto"> - <li data-sourcepos="1:1-1:10" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled> - <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="issue">1 (#48)</a> - </li> - <li data-sourcepos="2:1-2:7" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled> 2 - </li> - <li data-sourcepos="3:1-3:7" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled> 3 - </li> - </ul> -`; |