summaryrefslogtreecommitdiff
path: root/spec/frontend/issues
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/issues')
-rw-r--r--spec/frontend/issues/create_merge_request_dropdown_spec.js122
-rw-r--r--spec/frontend/issues/list/components/issue_card_time_info_spec.js122
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js829
-rw-r--r--spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js117
-rw-r--r--spec/frontend/issues/list/components/new_issue_dropdown_spec.js131
-rw-r--r--spec/frontend/issues/list/mock_data.js310
-rw-r--r--spec/frontend/issues/list/utils_spec.js127
-rw-r--r--spec/frontend/issues/new/components/title_suggestions_spec.js14
-rw-r--r--spec/frontend/issues/show/components/fields/type_spec.js18
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js19
-rw-r--r--spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js (renamed from spec/frontend/issues/sentry_error_stack_trace/components/sentry_error_stack_trace_spec.js)11
-rw-r--r--spec/frontend/issues/show/issue_spec.js6
12 files changed, 1798 insertions, 28 deletions
diff --git a/spec/frontend/issues/create_merge_request_dropdown_spec.js b/spec/frontend/issues/create_merge_request_dropdown_spec.js
new file mode 100644
index 00000000000..fdc0bd7d72e
--- /dev/null
+++ b/spec/frontend/issues/create_merge_request_dropdown_spec.js
@@ -0,0 +1,122 @@
+import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
+import confidentialState from '~/confidential_merge_request/state';
+import CreateMergeRequestDropdown from '~/issues/create_merge_request_dropdown';
+import axios from '~/lib/utils/axios_utils';
+
+describe('CreateMergeRequestDropdown', () => {
+ let axiosMock;
+ let dropdown;
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+
+ document.body.innerHTML = `
+ <div id="dummy-wrapper-element">
+ <div class="available"></div>
+ <div class="unavailable">
+ <div class="gl-spinner"></div>
+ <div class="text"></div>
+ </div>
+ <div class="js-ref"></div>
+ <div class="js-create-mr"></div>
+ <div class="js-create-merge-request">
+ <span class="js-spinner"></span>
+ </div>
+ <div class="js-create-target"></div>
+ <div class="js-dropdown-toggle"></div>
+ </div>
+ `;
+
+ const dummyElement = document.getElementById('dummy-wrapper-element');
+ dropdown = new CreateMergeRequestDropdown(dummyElement);
+ dropdown.refsPath = `${TEST_HOST}/dummy/refs?search=`;
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
+ describe('getRef', () => {
+ it('escapes branch names correctly', (done) => {
+ const endpoint = `${dropdown.refsPath}contains%23hash`;
+ jest.spyOn(axios, 'get');
+ axiosMock.onGet(endpoint).replyOnce({});
+
+ dropdown
+ .getRef('contains#hash')
+ .then(() => {
+ expect(axios.get).toHaveBeenCalledWith(
+ endpoint,
+ expect.objectContaining({ cancelToken: expect.anything() }),
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateCreatePaths', () => {
+ it('escapes branch names correctly', () => {
+ dropdown.createBranchPath = `${TEST_HOST}/branches?branch_name=some-branch&issue=42`;
+ dropdown.createMrPath = `${TEST_HOST}/create_merge_request?branch_name=some-branch&ref=main`;
+
+ dropdown.updateCreatePaths('branch', 'contains#hash');
+
+ expect(dropdown.createBranchPath).toBe(
+ `${TEST_HOST}/branches?branch_name=contains%23hash&issue=42`,
+ );
+
+ expect(dropdown.createMrPath).toBe(
+ `${TEST_HOST}/create_merge_request?branch_name=contains%23hash&ref=main`,
+ );
+ });
+ });
+
+ describe('enable', () => {
+ beforeEach(() => {
+ dropdown.createMergeRequestButton.classList.add('disabled');
+ });
+
+ afterEach(() => {
+ confidentialState.selectedProject = {};
+ });
+
+ it('enables button when not confidential issue', () => {
+ dropdown.enable();
+
+ expect(dropdown.createMergeRequestButton.classList).not.toContain('disabled');
+ });
+
+ it('enables when can create confidential issue', () => {
+ document.querySelector('.js-create-mr').setAttribute('data-is-confidential', 'true');
+ confidentialState.selectedProject = { name: 'test' };
+
+ dropdown.enable();
+
+ expect(dropdown.createMergeRequestButton.classList).not.toContain('disabled');
+ });
+
+ it('does not enable when can not create confidential issue', () => {
+ document.querySelector('.js-create-mr').setAttribute('data-is-confidential', 'true');
+
+ dropdown.enable();
+
+ expect(dropdown.createMergeRequestButton.classList).toContain('disabled');
+ });
+ });
+
+ describe('setLoading', () => {
+ it.each`
+ loading | hasClass
+ ${true} | ${false}
+ ${false} | ${true}
+ `('it toggle loading spinner when loading is $loading', ({ loading, hasClass }) => {
+ dropdown.setLoading(loading);
+
+ expect(document.querySelector('.js-spinner').classList.contains('gl-display-none')).toEqual(
+ hasClass,
+ );
+ });
+ });
+});
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
new file mode 100644
index 00000000000..e9c48b60da4
--- /dev/null
+++ b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
@@ -0,0 +1,122 @@
+import { GlIcon, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { useFakeDate } from 'helpers/fake_date';
+import IssueCardTimeInfo from '~/issues/list/components/issue_card_time_info.vue';
+
+describe('CE IssueCardTimeInfo component', () => {
+ useFakeDate(2020, 11, 11);
+
+ let wrapper;
+
+ const issue = {
+ milestone: {
+ dueDate: '2020-12-17',
+ startDate: '2020-12-10',
+ title: 'My milestone',
+ webPath: '/milestone/webPath',
+ },
+ dueDate: '2020-12-12',
+ humanTimeEstimate: '1w',
+ };
+
+ const findMilestone = () => wrapper.find('[data-testid="issuable-milestone"]');
+ const findMilestoneTitle = () => findMilestone().find(GlLink).attributes('title');
+ const findDueDate = () => wrapper.find('[data-testid="issuable-due-date"]');
+
+ const mountComponent = ({
+ closedAt = null,
+ dueDate = issue.dueDate,
+ milestoneDueDate = issue.milestone.dueDate,
+ milestoneStartDate = issue.milestone.startDate,
+ } = {}) =>
+ shallowMount(IssueCardTimeInfo, {
+ propsData: {
+ issue: {
+ ...issue,
+ milestone: {
+ ...issue.milestone,
+ dueDate: milestoneDueDate,
+ startDate: milestoneStartDate,
+ },
+ closedAt,
+ dueDate,
+ },
+ },
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('milestone', () => {
+ it('renders', () => {
+ wrapper = mountComponent();
+
+ const milestone = findMilestone();
+
+ expect(milestone.text()).toBe(issue.milestone.title);
+ expect(milestone.find(GlIcon).props('name')).toBe('clock');
+ expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webPath);
+ });
+
+ describe.each`
+ time | text | milestoneDueDate | milestoneStartDate | expected
+ ${'due date is in past'} | ${'Past due'} | ${'2020-09-09'} | ${null} | ${'Sep 9, 2020 (Past due)'}
+ ${'due date is today'} | ${'Today'} | ${'2020-12-11'} | ${null} | ${'Dec 11, 2020 (Today)'}
+ ${'start date is in future'} | ${'Upcoming'} | ${'2021-03-01'} | ${'2021-02-01'} | ${'Mar 1, 2021 (Upcoming)'}
+ ${'due date is in future'} | ${'2 weeks remaining'} | ${'2020-12-25'} | ${null} | ${'Dec 25, 2020 (2 weeks remaining)'}
+ `('when $description', ({ text, milestoneDueDate, milestoneStartDate, expected }) => {
+ it(`renders with "${text}"`, () => {
+ wrapper = mountComponent({ milestoneDueDate, milestoneStartDate });
+
+ expect(findMilestoneTitle()).toBe(expected);
+ });
+ });
+ });
+
+ describe('due date', () => {
+ describe('when upcoming', () => {
+ it('renders', () => {
+ wrapper = mountComponent();
+
+ const dueDate = findDueDate();
+
+ expect(dueDate.text()).toBe('Dec 12, 2020');
+ expect(dueDate.attributes('title')).toBe('Due date');
+ expect(dueDate.find(GlIcon).props('name')).toBe('calendar');
+ expect(dueDate.classes()).not.toContain('gl-text-red-500');
+ });
+ });
+
+ describe('when in the past', () => {
+ describe('when issue is open', () => {
+ it('renders in red', () => {
+ wrapper = mountComponent({ dueDate: new Date('2020-10-10') });
+
+ expect(findDueDate().classes()).toContain('gl-text-red-500');
+ });
+ });
+
+ describe('when issue is closed', () => {
+ it('does not render in red', () => {
+ wrapper = mountComponent({
+ dueDate: new Date('2020-10-10'),
+ closedAt: '2020-09-05T13:06:25Z',
+ });
+
+ expect(findDueDate().classes()).not.toContain('gl-text-red-500');
+ });
+ });
+ });
+ });
+
+ it('renders time estimate', () => {
+ wrapper = mountComponent();
+
+ const timeEstimate = wrapper.find('[data-testid="time-estimate"]');
+
+ expect(timeEstimate.text()).toBe(issue.humanTimeEstimate);
+ expect(timeEstimate.attributes('title')).toBe('Estimate');
+ expect(timeEstimate.find(GlIcon).props('name')).toBe('timer');
+ });
+});
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,
+ });
+ });
+ });
+ });
+ });
+});
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
new file mode 100644
index 00000000000..d6d6bb14e9d
--- /dev/null
+++ b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
@@ -0,0 +1,117 @@
+import { GlAlert, GlLabel } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import JiraIssuesImportStatus from '~/issues/list/components/jira_issues_import_status_app.vue';
+
+describe('JiraIssuesImportStatus', () => {
+ const issuesPath = 'gitlab-org/gitlab-test/-/issues';
+ const label = {
+ color: '#333',
+ title: 'jira-import::MTG-3',
+ };
+ let wrapper;
+
+ const findAlert = () => wrapper.find(GlAlert);
+
+ const findAlertLabel = () => wrapper.find(GlAlert).find(GlLabel);
+
+ const mountComponent = ({
+ shouldShowFinishedAlert = false,
+ shouldShowInProgressAlert = false,
+ } = {}) =>
+ shallowMount(JiraIssuesImportStatus, {
+ propsData: {
+ canEdit: true,
+ isJiraConfigured: true,
+ issuesPath,
+ projectPath: 'gitlab-org/gitlab-test',
+ },
+ data() {
+ return {
+ jiraImport: {
+ importedIssuesCount: 1,
+ label,
+ shouldShowFinishedAlert,
+ shouldShowInProgressAlert,
+ },
+ };
+ },
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when Jira import is neither in progress nor finished', () => {
+ beforeEach(() => {
+ wrapper = mountComponent();
+ });
+
+ it('does not show an alert', () => {
+ expect(wrapper.find(GlAlert).exists()).toBe(false);
+ });
+ });
+
+ describe('when Jira import is in progress', () => {
+ it('shows an alert that tells the user a Jira import is in progress', () => {
+ wrapper = mountComponent({
+ shouldShowInProgressAlert: true,
+ });
+
+ expect(findAlert().text()).toBe(
+ 'Import in progress. Refresh page to see newly added issues.',
+ );
+ });
+ });
+
+ describe('when Jira import has finished', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ shouldShowFinishedAlert: true,
+ });
+ });
+
+ describe('shows an alert', () => {
+ it('tells the user the Jira import has finished', () => {
+ expect(findAlert().text()).toBe('1 issue successfully imported with the label');
+ });
+
+ it('contains the label title associated with the Jira import', () => {
+ const alertLabelTitle = findAlertLabel().props('title');
+
+ expect(alertLabelTitle).toBe(label.title);
+ });
+
+ it('contains the correct label color', () => {
+ const alertLabelTitle = findAlertLabel().props('backgroundColor');
+
+ expect(alertLabelTitle).toBe(label.color);
+ });
+
+ it('contains a link within the label', () => {
+ const alertLabelTarget = findAlertLabel().props('target');
+
+ expect(alertLabelTarget).toBe(
+ `${issuesPath}?label_name[]=${encodeURIComponent(label.title)}`,
+ );
+ });
+ });
+ });
+
+ describe('alert message', () => {
+ it('is hidden when dismissed', () => {
+ wrapper = mountComponent({
+ shouldShowInProgressAlert: true,
+ });
+
+ expect(wrapper.find(GlAlert).exists()).toBe(true);
+
+ findAlert().vm.$emit('dismiss');
+
+ return Vue.nextTick(() => {
+ expect(wrapper.find(GlAlert).exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issues/list/components/new_issue_dropdown_spec.js b/spec/frontend/issues/list/components/new_issue_dropdown_spec.js
new file mode 100644
index 00000000000..0c52e66ff14
--- /dev/null
+++ b/spec/frontend/issues/list/components/new_issue_dropdown_spec.js
@@ -0,0 +1,131 @@
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
+import searchProjectsQuery from '~/issues/list/queries/search_projects.query.graphql';
+import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
+import {
+ emptySearchProjectsQueryResponse,
+ project1,
+ project3,
+ searchProjectsQueryResponse,
+} from '../mock_data';
+
+describe('NewIssueDropdown component', () => {
+ let wrapper;
+
+ const localVue = createLocalVue();
+ localVue.use(VueApollo);
+
+ const mountComponent = ({
+ search = '',
+ queryResponse = searchProjectsQueryResponse,
+ mountFn = shallowMount,
+ } = {}) => {
+ const requestHandlers = [[searchProjectsQuery, jest.fn().mockResolvedValue(queryResponse)]];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return mountFn(NewIssueDropdown, {
+ localVue,
+ apolloProvider,
+ provide: {
+ fullPath: 'mushroom-kingdom',
+ },
+ data() {
+ return { search };
+ },
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const showDropdown = async () => {
+ findDropdown().vm.$emit('shown');
+ await wrapper.vm.$apollo.queries.projects.refetch();
+ jest.runOnlyPendingTimers();
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a split dropdown', () => {
+ wrapper = mountComponent();
+
+ expect(findDropdown().props('split')).toBe(true);
+ });
+
+ it('renders a label for the dropdown toggle button', () => {
+ wrapper = mountComponent();
+
+ expect(findDropdown().attributes('toggle-text')).toBe(NewIssueDropdown.i18n.toggleButtonLabel);
+ });
+
+ it('focuses on input when dropdown is shown', async () => {
+ wrapper = mountComponent({ mountFn: mount });
+
+ const inputSpy = jest.spyOn(findInput().vm, 'focusInput');
+
+ await showDropdown();
+
+ expect(inputSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders projects with issues enabled', async () => {
+ wrapper = mountComponent({ mountFn: mount });
+
+ await showDropdown();
+
+ const listItems = wrapper.findAll('li');
+
+ expect(listItems.at(0).text()).toBe(project1.nameWithNamespace);
+ expect(listItems.at(1).text()).toBe(project3.nameWithNamespace);
+ });
+
+ it('renders `No matches found` when there are no matches', async () => {
+ wrapper = mountComponent({
+ search: 'no matches',
+ queryResponse: emptySearchProjectsQueryResponse,
+ mountFn: mount,
+ });
+
+ await showDropdown();
+
+ expect(wrapper.find('li').text()).toBe(NewIssueDropdown.i18n.noMatchesFound);
+ });
+
+ describe('when no project is selected', () => {
+ beforeEach(() => {
+ wrapper = mountComponent();
+ });
+
+ it('dropdown button is not a link', () => {
+ expect(findDropdown().attributes('split-href')).toBeUndefined();
+ });
+
+ it('displays default text on the dropdown button', () => {
+ expect(findDropdown().props('text')).toBe(NewIssueDropdown.i18n.defaultDropdownText);
+ });
+ });
+
+ describe('when a project is selected', () => {
+ beforeEach(async () => {
+ wrapper = mountComponent({ mountFn: mount });
+
+ await showDropdown();
+
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ });
+
+ it('dropdown button is a link', () => {
+ const href = joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new');
+
+ expect(findDropdown().attributes('split-href')).toBe(href);
+ });
+
+ it('displays project name on the dropdown button', () => {
+ expect(findDropdown().props('text')).toBe(`New issue in ${project1.name}`);
+ });
+ });
+});
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
new file mode 100644
index 00000000000..948699876ce
--- /dev/null
+++ b/spec/frontend/issues/list/mock_data.js
@@ -0,0 +1,310 @@
+import {
+ OPERATOR_IS,
+ OPERATOR_IS_NOT,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+
+export const getIssuesQueryResponse = {
+ data: {
+ project: {
+ id: '1',
+ issues: {
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'startcursor',
+ endCursor: 'endcursor',
+ },
+ nodes: [
+ {
+ id: 'gid://gitlab/Issue/123456',
+ iid: '789',
+ closedAt: null,
+ confidential: false,
+ createdAt: '2021-05-22T04:08:01Z',
+ downvotes: 2,
+ dueDate: '2021-05-29',
+ hidden: false,
+ humanTimeEstimate: null,
+ mergeRequestsCount: false,
+ moved: false,
+ title: 'Issue title',
+ updatedAt: '2021-05-22T04:08:01Z',
+ upvotes: 3,
+ userDiscussionsCount: 4,
+ webPath: 'project/-/issues/789',
+ webUrl: 'project/-/issues/789',
+ assignees: {
+ nodes: [
+ {
+ id: 'gid://gitlab/User/234',
+ avatarUrl: 'avatar/url',
+ name: 'Marge Simpson',
+ username: 'msimpson',
+ webUrl: 'url/msimpson',
+ },
+ ],
+ },
+ author: {
+ id: 'gid://gitlab/User/456',
+ avatarUrl: 'avatar/url',
+ name: 'Homer Simpson',
+ username: 'hsimpson',
+ webUrl: 'url/hsimpson',
+ },
+ labels: {
+ nodes: [
+ {
+ id: 'gid://gitlab/ProjectLabel/456',
+ color: '#333',
+ title: 'Label title',
+ description: 'Label description',
+ },
+ ],
+ },
+ milestone: null,
+ taskCompletionStatus: {
+ completedCount: 1,
+ count: 2,
+ },
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const getIssuesCountsQueryResponse = {
+ data: {
+ project: {
+ id: '1',
+ openedIssues: {
+ count: 1,
+ },
+ closedIssues: {
+ count: 1,
+ },
+ allIssues: {
+ count: 1,
+ },
+ },
+ },
+};
+
+export const locationSearch = [
+ '?search=find+issues',
+ 'author_username=homer',
+ 'not[author_username]=marge',
+ 'assignee_username[]=bart',
+ 'assignee_username[]=lisa',
+ 'not[assignee_username][]=patty',
+ 'not[assignee_username][]=selma',
+ 'milestone_title=season+3',
+ 'milestone_title=season+4',
+ 'not[milestone_title]=season+20',
+ 'not[milestone_title]=season+30',
+ 'label_name[]=cartoon',
+ 'label_name[]=tv',
+ 'not[label_name][]=live action',
+ 'not[label_name][]=drama',
+ 'release_tag=v3',
+ 'release_tag=v4',
+ 'not[release_tag]=v20',
+ 'not[release_tag]=v30',
+ 'type[]=issue',
+ 'type[]=feature',
+ 'not[type][]=bug',
+ 'not[type][]=incident',
+ 'my_reaction_emoji=thumbsup',
+ 'not[my_reaction_emoji]=thumbsdown',
+ 'confidential=yes',
+ 'iteration_id=4',
+ 'iteration_id=12',
+ 'not[iteration_id]=20',
+ 'not[iteration_id]=42',
+ 'epic_id=12',
+ 'not[epic_id]=34',
+ 'weight=1',
+ 'not[weight]=3',
+].join('&');
+
+export const locationSearchWithSpecialValues = [
+ 'assignee_id=123',
+ 'assignee_username=bart',
+ 'my_reaction_emoji=None',
+ 'iteration_id=Current',
+ 'label_name[]=None',
+ 'release_tag=None',
+ 'milestone_title=Upcoming',
+ 'epic_id=None',
+ 'weight=None',
+].join('&');
+
+export const filteredTokens = [
+ { type: 'author_username', value: { data: 'homer', operator: OPERATOR_IS } },
+ { type: 'author_username', value: { data: 'marge', operator: OPERATOR_IS_NOT } },
+ { type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } },
+ { type: 'assignee_username', value: { data: 'lisa', operator: OPERATOR_IS } },
+ { type: 'assignee_username', value: { data: 'patty', operator: OPERATOR_IS_NOT } },
+ { type: 'assignee_username', value: { data: 'selma', operator: OPERATOR_IS_NOT } },
+ { type: 'milestone', value: { data: 'season 3', operator: OPERATOR_IS } },
+ { type: 'milestone', value: { data: 'season 4', operator: OPERATOR_IS } },
+ { type: 'milestone', value: { data: 'season 20', operator: OPERATOR_IS_NOT } },
+ { type: 'milestone', value: { data: 'season 30', operator: OPERATOR_IS_NOT } },
+ { type: 'labels', value: { data: 'cartoon', operator: OPERATOR_IS } },
+ { type: 'labels', value: { data: 'tv', operator: OPERATOR_IS } },
+ { type: 'labels', value: { data: 'live action', operator: OPERATOR_IS_NOT } },
+ { type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } },
+ { type: 'release', value: { data: 'v3', operator: OPERATOR_IS } },
+ { type: 'release', value: { data: 'v4', operator: OPERATOR_IS } },
+ { type: 'release', value: { data: 'v20', operator: OPERATOR_IS_NOT } },
+ { type: 'release', value: { data: 'v30', operator: OPERATOR_IS_NOT } },
+ { type: 'type', value: { data: 'issue', operator: OPERATOR_IS } },
+ { type: 'type', value: { data: 'feature', operator: OPERATOR_IS } },
+ { type: 'type', value: { data: 'bug', operator: OPERATOR_IS_NOT } },
+ { type: 'type', value: { data: 'incident', operator: OPERATOR_IS_NOT } },
+ { type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } },
+ { type: 'my_reaction_emoji', value: { data: 'thumbsdown', operator: OPERATOR_IS_NOT } },
+ { type: 'confidential', value: { data: 'yes', operator: OPERATOR_IS } },
+ { type: 'iteration', value: { data: '4', operator: OPERATOR_IS } },
+ { type: 'iteration', value: { data: '12', operator: OPERATOR_IS } },
+ { type: 'iteration', value: { data: '20', operator: OPERATOR_IS_NOT } },
+ { type: 'iteration', value: { data: '42', operator: OPERATOR_IS_NOT } },
+ { type: 'epic_id', value: { data: '12', operator: OPERATOR_IS } },
+ { type: 'epic_id', value: { data: '34', operator: OPERATOR_IS_NOT } },
+ { type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
+ { type: 'weight', value: { data: '3', operator: OPERATOR_IS_NOT } },
+ { type: 'filtered-search-term', value: { data: 'find' } },
+ { type: 'filtered-search-term', value: { data: 'issues' } },
+];
+
+export const filteredTokensWithSpecialValues = [
+ { type: 'assignee_username', value: { data: '123', operator: OPERATOR_IS } },
+ { type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } },
+ { type: 'my_reaction_emoji', value: { data: 'None', operator: OPERATOR_IS } },
+ { type: 'iteration', value: { data: 'Current', operator: OPERATOR_IS } },
+ { type: 'labels', value: { data: 'None', operator: OPERATOR_IS } },
+ { type: 'release', value: { data: 'None', operator: OPERATOR_IS } },
+ { type: 'milestone', value: { data: 'Upcoming', operator: OPERATOR_IS } },
+ { type: 'epic_id', value: { data: 'None', operator: OPERATOR_IS } },
+ { type: 'weight', value: { data: 'None', operator: OPERATOR_IS } },
+];
+
+export const apiParams = {
+ authorUsername: 'homer',
+ assigneeUsernames: ['bart', 'lisa'],
+ milestoneTitle: ['season 3', 'season 4'],
+ labelName: ['cartoon', 'tv'],
+ releaseTag: ['v3', 'v4'],
+ types: ['ISSUE', 'FEATURE'],
+ myReactionEmoji: 'thumbsup',
+ confidential: true,
+ iterationId: ['4', '12'],
+ epicId: '12',
+ weight: '1',
+ not: {
+ authorUsername: 'marge',
+ assigneeUsernames: ['patty', 'selma'],
+ milestoneTitle: ['season 20', 'season 30'],
+ labelName: ['live action', 'drama'],
+ releaseTag: ['v20', 'v30'],
+ types: ['BUG', 'INCIDENT'],
+ myReactionEmoji: 'thumbsdown',
+ iterationId: ['20', '42'],
+ epicId: '34',
+ weight: '3',
+ },
+};
+
+export const apiParamsWithSpecialValues = {
+ assigneeId: '123',
+ assigneeUsernames: 'bart',
+ labelName: 'None',
+ myReactionEmoji: 'None',
+ releaseTagWildcardId: 'NONE',
+ iterationWildcardId: 'CURRENT',
+ milestoneWildcardId: 'UPCOMING',
+ epicId: 'None',
+ weight: 'None',
+};
+
+export const urlParams = {
+ author_username: 'homer',
+ 'not[author_username]': 'marge',
+ 'assignee_username[]': ['bart', 'lisa'],
+ 'not[assignee_username][]': ['patty', 'selma'],
+ milestone_title: ['season 3', 'season 4'],
+ 'not[milestone_title]': ['season 20', 'season 30'],
+ 'label_name[]': ['cartoon', 'tv'],
+ 'not[label_name][]': ['live action', 'drama'],
+ release_tag: ['v3', 'v4'],
+ 'not[release_tag]': ['v20', 'v30'],
+ 'type[]': ['issue', 'feature'],
+ 'not[type][]': ['bug', 'incident'],
+ my_reaction_emoji: 'thumbsup',
+ 'not[my_reaction_emoji]': 'thumbsdown',
+ confidential: 'yes',
+ iteration_id: ['4', '12'],
+ 'not[iteration_id]': ['20', '42'],
+ epic_id: '12',
+ 'not[epic_id]': '34',
+ weight: '1',
+ 'not[weight]': '3',
+};
+
+export const urlParamsWithSpecialValues = {
+ assignee_id: '123',
+ 'assignee_username[]': 'bart',
+ 'label_name[]': 'None',
+ release_tag: 'None',
+ my_reaction_emoji: 'None',
+ iteration_id: 'Current',
+ milestone_title: 'Upcoming',
+ epic_id: 'None',
+ weight: 'None',
+};
+
+export const project1 = {
+ id: 'gid://gitlab/Group/26',
+ issuesEnabled: true,
+ name: 'Super Mario Project',
+ nameWithNamespace: 'Mushroom Kingdom / Super Mario Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/super-mario-project',
+};
+
+export const project2 = {
+ id: 'gid://gitlab/Group/59',
+ issuesEnabled: false,
+ name: 'Mario Kart Project',
+ nameWithNamespace: 'Mushroom Kingdom / Mario Kart Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-kart-project',
+};
+
+export const project3 = {
+ id: 'gid://gitlab/Group/103',
+ issuesEnabled: true,
+ name: 'Mario Party Project',
+ nameWithNamespace: 'Mushroom Kingdom / Mario Party Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-party-project',
+};
+
+export const searchProjectsQueryResponse = {
+ data: {
+ group: {
+ id: '1',
+ projects: {
+ nodes: [project1, project2, project3],
+ },
+ },
+ },
+};
+
+export const emptySearchProjectsQueryResponse = {
+ data: {
+ group: {
+ id: '1',
+ projects: {
+ nodes: [],
+ },
+ },
+ },
+};
diff --git a/spec/frontend/issues/list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js
new file mode 100644
index 00000000000..0e4979fd7b4
--- /dev/null
+++ b/spec/frontend/issues/list/utils_spec.js
@@ -0,0 +1,127 @@
+import {
+ apiParams,
+ apiParamsWithSpecialValues,
+ filteredTokens,
+ filteredTokensWithSpecialValues,
+ locationSearch,
+ locationSearchWithSpecialValues,
+ urlParams,
+ urlParamsWithSpecialValues,
+} from 'jest/issues/list/mock_data';
+import {
+ defaultPageSizeParams,
+ DUE_DATE_VALUES,
+ largePageSizeParams,
+ RELATIVE_POSITION_ASC,
+ urlSortParams,
+} from '~/issues/list/constants';
+import {
+ convertToApiParams,
+ convertToSearchQuery,
+ convertToUrlParams,
+ getDueDateValue,
+ getFilterTokens,
+ getInitialPageParams,
+ getSortKey,
+ getSortOptions,
+} from '~/issues/list/utils';
+
+describe('getInitialPageParams', () => {
+ it.each(Object.keys(urlSortParams))(
+ 'returns the correct page params for sort key %s',
+ (sortKey) => {
+ const expectedPageParams =
+ sortKey === RELATIVE_POSITION_ASC ? largePageSizeParams : defaultPageSizeParams;
+
+ expect(getInitialPageParams(sortKey)).toBe(expectedPageParams);
+ },
+ );
+});
+
+describe('getSortKey', () => {
+ it.each(Object.keys(urlSortParams))('returns %s given the correct inputs', (sortKey) => {
+ const sort = urlSortParams[sortKey];
+ expect(getSortKey(sort)).toBe(sortKey);
+ });
+});
+
+describe('getDueDateValue', () => {
+ it.each(DUE_DATE_VALUES)('returns the argument when it is `%s`', (value) => {
+ expect(getDueDateValue(value)).toBe(value);
+ });
+
+ it('returns undefined when the argument is invalid', () => {
+ expect(getDueDateValue('invalid value')).toBeUndefined();
+ });
+});
+
+describe('getSortOptions', () => {
+ describe.each`
+ hasIssueWeightsFeature | hasBlockedIssuesFeature | length | containsWeight | containsBlocking
+ ${false} | ${false} | ${9} | ${false} | ${false}
+ ${true} | ${false} | ${10} | ${true} | ${false}
+ ${false} | ${true} | ${10} | ${false} | ${true}
+ ${true} | ${true} | ${11} | ${true} | ${true}
+ `(
+ 'when hasIssueWeightsFeature=$hasIssueWeightsFeature and hasBlockedIssuesFeature=$hasBlockedIssuesFeature',
+ ({
+ hasIssueWeightsFeature,
+ hasBlockedIssuesFeature,
+ length,
+ containsWeight,
+ containsBlocking,
+ }) => {
+ const sortOptions = getSortOptions(hasIssueWeightsFeature, hasBlockedIssuesFeature);
+
+ it('returns the correct length of sort options', () => {
+ expect(sortOptions).toHaveLength(length);
+ });
+
+ it(`${containsWeight ? 'contains' : 'does not contain'} weight option`, () => {
+ expect(sortOptions.some((option) => option.title === 'Weight')).toBe(containsWeight);
+ });
+
+ it(`${containsBlocking ? 'contains' : 'does not contain'} blocking option`, () => {
+ expect(sortOptions.some((option) => option.title === 'Blocking')).toBe(containsBlocking);
+ });
+ },
+ );
+});
+
+describe('getFilterTokens', () => {
+ it('returns filtered tokens given "window.location.search"', () => {
+ expect(getFilterTokens(locationSearch)).toEqual(filteredTokens);
+ });
+
+ it('returns filtered tokens given "window.location.search" with special values', () => {
+ expect(getFilterTokens(locationSearchWithSpecialValues)).toEqual(
+ filteredTokensWithSpecialValues,
+ );
+ });
+});
+
+describe('convertToApiParams', () => {
+ it('returns api params given filtered tokens', () => {
+ expect(convertToApiParams(filteredTokens)).toEqual(apiParams);
+ });
+
+ it('returns api params given filtered tokens with special values', () => {
+ expect(convertToApiParams(filteredTokensWithSpecialValues)).toEqual(apiParamsWithSpecialValues);
+ });
+});
+
+describe('convertToUrlParams', () => {
+ it('returns url params given filtered tokens', () => {
+ expect(convertToUrlParams(filteredTokens)).toEqual(urlParams);
+ });
+
+ it('returns url params given filtered tokens with special values', () => {
+ expect(convertToUrlParams(filteredTokensWithSpecialValues)).toEqual(urlParamsWithSpecialValues);
+ });
+});
+
+describe('convertToSearchQuery', () => {
+ it('returns search string given filtered tokens', () => {
+ expect(convertToSearchQuery(filteredTokens)).toBe('find issues');
+ });
+});
diff --git a/spec/frontend/issues/new/components/title_suggestions_spec.js b/spec/frontend/issues/new/components/title_suggestions_spec.js
index 984d0c9d25b..f6b93cc5a62 100644
--- a/spec/frontend/issues/new/components/title_suggestions_spec.js
+++ b/spec/frontend/issues/new/components/title_suggestions_spec.js
@@ -38,6 +38,8 @@ describe('Issue title suggestions component', () => {
});
it('renders component', () => {
+ // 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);
return wrapper.vm.$nextTick(() => {
@@ -47,6 +49,8 @@ describe('Issue title suggestions component', () => {
it('does not render with empty search', () => {
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);
return wrapper.vm.$nextTick(() => {
@@ -55,6 +59,8 @@ describe('Issue title suggestions component', () => {
});
it('does not render when loading', () => {
+ // 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,
@@ -66,6 +72,8 @@ describe('Issue title suggestions component', () => {
});
it('does not render with empty issues data', () => {
+ // 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: [] });
return wrapper.vm.$nextTick(() => {
@@ -74,6 +82,8 @@ describe('Issue title suggestions component', () => {
});
it('renders list of issues', () => {
+ // 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);
return wrapper.vm.$nextTick(() => {
@@ -82,6 +92,8 @@ describe('Issue title suggestions component', () => {
});
it('adds margin class to first item', () => {
+ // 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);
return wrapper.vm.$nextTick(() => {
@@ -90,6 +102,8 @@ describe('Issue title suggestions component', () => {
});
it('does not add margin class to last item', () => {
+ // 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);
return wrapper.vm.$nextTick(() => {
diff --git a/spec/frontend/issues/show/components/fields/type_spec.js b/spec/frontend/issues/show/components/fields/type_spec.js
index 3ece10e70db..7f7b16583e6 100644
--- a/spec/frontend/issues/show/components/fields/type_spec.js
+++ b/spec/frontend/issues/show/components/fields/type_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import IssueTypeField, { i18n } from '~/issues/show/components/fields/type.vue';
-import { IssuableTypes } from '~/issues/show/constants';
+import { issuableTypes } from '~/issues/show/constants';
import {
getIssueStateQueryResponse,
updateIssueStateQueryResponse,
@@ -69,8 +69,8 @@ describe('Issue type field component', () => {
it.each`
at | text | icon
- ${0} | ${IssuableTypes[0].text} | ${IssuableTypes[0].icon}
- ${1} | ${IssuableTypes[1].text} | ${IssuableTypes[1].icon}
+ ${0} | ${issuableTypes[0].text} | ${issuableTypes[0].icon}
+ ${1} | ${issuableTypes[1].text} | ${issuableTypes[1].icon}
`(`renders the issue type $text with an icon in the dropdown`, ({ at, text, icon }) => {
expect(findTypeFromDropDownItemIconAt(at).attributes('name')).toBe(icon);
expect(findTypeFromDropDownItemAt(at).text()).toBe(text);
@@ -81,20 +81,20 @@ describe('Issue type field component', () => {
});
it('renders a form select with the `issue_type` value', () => {
- expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
+ expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
});
describe('with Apollo cache mock', () => {
it('renders the selected issueType', async () => {
mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse);
await waitForPromises();
- expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
+ expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
});
it('updates the `issue_type` in the apollo cache when the value is changed', async () => {
- findTypeFromDropDownItems().at(1).vm.$emit('click', IssuableTypes.incident);
+ findTypeFromDropDownItems().at(1).vm.$emit('click', issuableTypes.incident);
await wrapper.vm.$nextTick();
- expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
+ expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.incident);
});
describe('when user is a guest', () => {
@@ -104,7 +104,7 @@ describe('Issue type field component', () => {
expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(false);
- expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
+ expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
});
it('and incident is selected, includes incident in the dropdown', async () => {
@@ -113,7 +113,7 @@ describe('Issue type field component', () => {
expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(true);
- expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
+ expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.incident);
});
});
});
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index 2a16c699c4d..d09bf6faa13 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -4,11 +4,10 @@ import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import createFlash, { FLASH_TYPES } from '~/flash';
-import { IssuableType } from '~/vue_shared/issuable/show/constants';
+import { IssuableStatus, IssueType } from '~/issues/constants';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
import HeaderActions from '~/issues/show/components/header_actions.vue';
-import { IssuableStatus } from '~/issues/constants';
-import { IssueStateEvent } from '~/issues/show/constants';
+import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
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';
@@ -36,7 +35,7 @@ describe('HeaderActions component', () => {
iid: '32',
isIssueAuthor: true,
issuePath: 'gitlab-org/gitlab-test/-/issues/1',
- issueType: IssuableType.Issue,
+ issueType: IssueType.Issue,
newIssuePath: 'gitlab-org/gitlab-test/-/issues/new',
projectPath: 'gitlab-org/gitlab-test',
reportAbusePath:
@@ -112,14 +111,14 @@ describe('HeaderActions component', () => {
describe.each`
issueType
- ${IssuableType.Issue}
- ${IssuableType.Incident}
+ ${IssueType.Issue}
+ ${IssueType.Incident}
`('when issue type is $issueType', ({ issueType }) => {
describe('close/reopen button', () => {
describe.each`
description | issueState | buttonText | newIssueState
- ${`when the ${issueType} is open`} | ${IssuableStatus.Open} | ${`Close ${issueType}`} | ${IssueStateEvent.Close}
- ${`when the ${issueType} is closed`} | ${IssuableStatus.Closed} | ${`Reopen ${issueType}`} | ${IssueStateEvent.Reopen}
+ ${`when the ${issueType} is open`} | ${IssuableStatus.Open} | ${`Close ${issueType}`} | ${ISSUE_STATE_EVENT_CLOSE}
+ ${`when the ${issueType} is closed`} | ${IssuableStatus.Closed} | ${`Reopen ${issueType}`} | ${ISSUE_STATE_EVENT_REOPEN}
`('$description', ({ issueState, buttonText, newIssueState }) => {
beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
@@ -306,7 +305,7 @@ describe('HeaderActions component', () => {
input: {
iid: defaultProps.iid,
projectPath: defaultProps.projectPath,
- stateEvent: IssueStateEvent.Close,
+ stateEvent: ISSUE_STATE_EVENT_CLOSE,
},
},
}),
@@ -345,7 +344,7 @@ describe('HeaderActions component', () => {
input: {
iid: defaultProps.iid.toString(),
projectPath: defaultProps.projectPath,
- stateEvent: IssueStateEvent.Close,
+ stateEvent: ISSUE_STATE_EVENT_CLOSE,
},
},
}),
diff --git a/spec/frontend/issues/sentry_error_stack_trace/components/sentry_error_stack_trace_spec.js b/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js
index 5a51ae3cfe0..b38d2b60057 100644
--- a/spec/frontend/issues/sentry_error_stack_trace/components/sentry_error_stack_trace_spec.js
+++ b/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js
@@ -1,11 +1,9 @@
+import Vue from 'vue';
import { GlLoadingIcon } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import Stacktrace from '~/error_tracking/components/stacktrace.vue';
-import SentryErrorStackTrace from '~/issues/sentry_error_stack_trace/components/sentry_error_stack_trace.vue';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
+import SentryErrorStackTrace from '~/issues/show/components/sentry_error_stack_trace.vue';
describe('Sentry Error Stack Trace', () => {
let actions;
@@ -13,13 +11,14 @@ describe('Sentry Error Stack Trace', () => {
let store;
let wrapper;
+ Vue.use(Vuex);
+
function mountComponent({
stubs = {
stacktrace: Stacktrace,
},
} = {}) {
wrapper = shallowMount(SentryErrorStackTrace, {
- localVue,
stubs,
store,
propsData: {
diff --git a/spec/frontend/issues/show/issue_spec.js b/spec/frontend/issues/show/issue_spec.js
index 6d7a31a6c8c..68c2e3768c7 100644
--- a/spec/frontend/issues/show/issue_spec.js
+++ b/spec/frontend/issues/show/issue_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import { initIssuableApp } from '~/issues/show/issue';
+import { initIssueApp } from '~/issues/show';
import * as parseData from '~/issues/show/utils/parse_data';
import axios from '~/lib/utils/axios_utils';
import createStore from '~/notes/stores';
@@ -17,7 +17,7 @@ const setupHTML = (initialData) => {
};
describe('Issue show index', () => {
- describe('initIssuableApp', () => {
+ describe('initIssueApp', () => {
it('should initialize app with no potential XSS attack', async () => {
const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
const parseDataSpy = jest.spyOn(parseData, 'parseIssuableData');
@@ -29,7 +29,7 @@ describe('Issue show index', () => {
const initialDataEl = document.getElementById('js-issuable-app');
const issuableData = parseData.parseIssuableData(initialDataEl);
- initIssuableApp(issuableData, createStore());
+ initIssueApp(issuableData, createStore());
await waitForPromises();