791 lines
26 KiB
JavaScript
791 lines
26 KiB
JavaScript
import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
|
|
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
|
|
import AxiosMockAdapter from 'axios-mock-adapter';
|
|
import { cloneDeep } from 'lodash';
|
|
import { 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 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 '~/issuable_list/components/issuable_list_root.vue';
|
|
import { IssuableListTabs, IssuableStates } from '~/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,
|
|
TOKEN_TYPE_ASSIGNEE,
|
|
TOKEN_TYPE_AUTHOR,
|
|
TOKEN_TYPE_CONFIDENTIAL,
|
|
TOKEN_TYPE_EPIC,
|
|
TOKEN_TYPE_ITERATION,
|
|
TOKEN_TYPE_LABEL,
|
|
TOKEN_TYPE_MILESTONE,
|
|
TOKEN_TYPE_MY_REACTION,
|
|
TOKEN_TYPE_TYPE,
|
|
TOKEN_TYPE_WEIGHT,
|
|
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('~/flash');
|
|
jest.mock('~/lib/utils/scroll_utils', () => ({
|
|
scrollUp: jest.fn().mockName('scrollUpMock'),
|
|
}));
|
|
|
|
describe('IssuesListApp component', () => {
|
|
let axiosMock;
|
|
let wrapper;
|
|
|
|
const localVue = createLocalVue();
|
|
localVue.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,
|
|
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, {
|
|
localVue,
|
|
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('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('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 iterations are not available', () => {
|
|
beforeEach(() => {
|
|
wrapper = mountComponent({
|
|
provide: {
|
|
projectIterationsPath: '',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('does not render Iteration token', () => {
|
|
expect(findIssuableList().props('searchTokens')).not.toMatchObject([
|
|
{ type: TOKEN_TYPE_ITERATION },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('when epics are not available', () => {
|
|
beforeEach(() => {
|
|
wrapper = mountComponent({
|
|
provide: {
|
|
groupPath: '',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('does not render Epic token', () => {
|
|
expect(findIssuableList().props('searchTokens')).not.toMatchObject([
|
|
{ type: TOKEN_TYPE_EPIC },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('when weights are not available', () => {
|
|
beforeEach(() => {
|
|
wrapper = mountComponent({
|
|
provide: {
|
|
groupPath: '',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('does not render Weight token', () => {
|
|
expect(findIssuableList().props('searchTokens')).not.toMatchObject([
|
|
{ type: TOKEN_TYPE_WEIGHT },
|
|
]);
|
|
});
|
|
});
|
|
|
|
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,
|
|
projectIterationsPath: 'project/iterations/path',
|
|
groupPath: 'group/path',
|
|
hasIssueWeightsFeature: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('renders all tokens', () => {
|
|
const preloadedAuthors = [
|
|
{ ...mockCurrentUser, id: convertToGraphQLId('User', mockCurrentUser.id) },
|
|
];
|
|
|
|
expect(findIssuableList().props('searchTokens')).toMatchObject([
|
|
{ type: TOKEN_TYPE_AUTHOR, preloadedAuthors },
|
|
{ type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors },
|
|
{ type: TOKEN_TYPE_MILESTONE },
|
|
{ type: TOKEN_TYPE_LABEL },
|
|
{ type: TOKEN_TYPE_TYPE },
|
|
{ type: TOKEN_TYPE_MY_REACTION },
|
|
{ type: TOKEN_TYPE_CONFIDENTIAL },
|
|
{ type: TOKEN_TYPE_ITERATION },
|
|
{ type: TOKEN_TYPE_EPIC },
|
|
{ type: TOKEN_TYPE_WEIGHT },
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
|
|
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(createFlash).toHaveBeenCalledWith({
|
|
captureError: true,
|
|
error: new Error('Network error: ERROR'),
|
|
message,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
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']: {
|
|
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(createFlash).toHaveBeenCalledWith({
|
|
message: IssuesListApp.i18n.reorderError,
|
|
captureError: true,
|
|
error: 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 "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', () => {
|
|
beforeEach(() => {
|
|
wrapper = mountComponent();
|
|
|
|
findIssuableList().vm.$emit('filter', filteredTokens);
|
|
});
|
|
|
|
it('updates IssuableList with url params', () => {
|
|
expect(findIssuableList().props('urlParams')).toMatchObject(urlParams);
|
|
});
|
|
});
|
|
});
|
|
});
|