Commit 2d8961cc authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'jira-issues-root-cleanup' into 'master'

Jira issues root cleanup

See merge request gitlab-org/gitlab!60468
parents e8a8089b 99d42aed
...@@ -274,9 +274,10 @@ export default { ...@@ -274,9 +274,10 @@ export default {
<gl-skeleton-loading /> <gl-skeleton-loading />
</li> </li>
</ul> </ul>
<template v-else>
<component <component
:is="issuablesWrapper" :is="issuablesWrapper"
v-if="!issuablesLoading && issuables.length" v-if="issuables.length > 0"
class="content-list issuable-list issues-list" class="content-list issuable-list issues-list"
:class="{ 'manual-ordering': isManualOrdering }" :class="{ 'manual-ordering': isManualOrdering }"
v-bind="$options.vueDraggableAttributes" v-bind="$options.vueDraggableAttributes"
...@@ -311,7 +312,9 @@ export default { ...@@ -311,7 +312,9 @@ export default {
</template> </template>
</issuable-item> </issuable-item>
</component> </component>
<slot v-if="!issuablesLoading && !issuables.length" name="empty-state"></slot> <slot v-else name="empty-state"></slot>
</template>
<gl-pagination <gl-pagination
v-if="showPaginationControls" v-if="showPaginationControls"
:per-page="defaultPageSize" :per-page="defaultPageSize"
......
...@@ -48,12 +48,13 @@ export default { ...@@ -48,12 +48,13 @@ export default {
<template #title> <template #title>
<span :title="tab.titleTooltip">{{ tab.title }}</span> <span :title="tab.titleTooltip">{{ tab.title }}</span>
<gl-badge <gl-badge
v-if="isTabCountNumeric(tab)" v-if="tabCounts && isTabCountNumeric(tab)"
variant="neutral" variant="neutral"
size="sm" size="sm"
class="gl-tab-counter-badge" class="gl-tab-counter-badge"
>{{ tabCounts[tab.name] }}</gl-badge
> >
{{ tabCounts[tab.name] }}
</gl-badge>
</template> </template>
</gl-tab> </gl-tab>
</gl-tabs> </gl-tabs>
......
...@@ -206,19 +206,19 @@ export default { ...@@ -206,19 +206,19 @@ export default {
</template> </template>
<template #reference="{ issuable }"> <template #reference="{ issuable }">
<span v-safe-html="jiraLogo" class="svg-container jira-logo-container"></span> <span v-safe-html="jiraLogo" class="svg-container jira-logo-container"></span>
<span>{{ issuable.references.relative }}</span> <span v-if="issuable">{{ issuable.references.relative }}</span>
</template> </template>
<template #author="{ author }"> <template #author="{ author }">
<gl-sprintf message="%{authorName} in Jira"> <gl-sprintf message="%{authorName} in Jira">
<template #authorName> <template #authorName>
<gl-link class="author-link js-user-link" target="_blank" :href="author.webUrl" <gl-link class="author-link js-user-link" target="_blank" :href="author.webUrl">
>{{ author.name }} {{ author.name }}
</gl-link> </gl-link>
</template> </template>
</gl-sprintf> </gl-sprintf>
</template> </template>
<template #status="{ issuable }"> <template #status="{ issuable }">
{{ issuable.status }} <template v-if="issuable"> {{ issuable.status }} </template>
</template> </template>
<template #empty-state> <template #empty-state>
<jira-issues-list-empty-state <jira-issues-list-empty-state
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`JiraIssuesListRoot renders issuable-list component with correct props 1`] = `
Object {
"currentPage": 1,
"currentTab": "opened",
"defaultPageSize": 2,
"enableLabelPermalinks": true,
"initialFilterValue": Array [
Object {
"type": "filtered-search-term",
"value": Object {
"data": "foo",
},
},
],
"initialSortBy": "created_desc",
"isManualOrdering": false,
"issuableSymbol": "#",
"issuables": Array [],
"issuablesLoading": false,
"labelFilterParam": "labels",
"namespace": "gitlab-org/gitlab-test",
"nextPage": 2,
"previousPage": 0,
"recentSearchesStorageKey": "jira_issues",
"searchInputPlaceholder": "Search Jira issues",
"searchTokens": Array [],
"showBulkEditSidebar": false,
"showPaginationControls": false,
"sortOptions": Array [
Object {
"id": 1,
"sortDirection": Object {
"ascending": "created_asc",
"descending": "created_desc",
},
"title": "Created date",
},
Object {
"id": 2,
"sortDirection": Object {
"ascending": "updated_asc",
"descending": "updated_desc",
},
"title": "Last updated",
},
],
"tabCounts": null,
"tabs": Array [
Object {
"id": "state-opened",
"name": "opened",
"title": "Open",
"titleTooltip": "Filter by issues that are currently opened.",
},
Object {
"id": "state-closed",
"name": "closed",
"title": "Closed",
"titleTooltip": "Filter by issues that are currently closed.",
},
Object {
"id": "state-all",
"name": "all",
"title": "All",
"titleTooltip": "Show all issues.",
},
],
"totalItems": 0,
"urlParams": Object {
"labels[]": undefined,
"page": 1,
"search": "foo",
"sort": "created_desc",
"state": "opened",
},
}
`;
...@@ -2,13 +2,12 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,13 +2,12 @@ import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import JiraIssuesListRoot from 'ee/integrations/jira/issues_list/components/jira_issues_list_root.vue'; import JiraIssuesListRoot from 'ee/integrations/jira/issues_list/components/jira_issues_list_root.vue';
import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash'; import createFlash from '~/flash';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
import { IssuableStates, IssuableListTabs, AvailableSortOptions } from '~/issuable_list/constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import httpStatus from '~/lib/utils/http_status';
import { mockProvide, mockJiraIssues } from '../mock_data'; import { mockProvide, mockJiraIssues } from '../mock_data';
...@@ -20,31 +19,31 @@ jest.mock('~/issuable_list/constants', () => ({ ...@@ -20,31 +19,31 @@ jest.mock('~/issuable_list/constants', () => ({
AvailableSortOptions: jest.requireActual('~/issuable_list/constants').AvailableSortOptions, AvailableSortOptions: jest.requireActual('~/issuable_list/constants').AvailableSortOptions,
})); }));
const createComponent = ({ provide = mockProvide, initialFilterParams = {} } = {}) => const resolvedValue = {
shallowMount(JiraIssuesListRoot, {
propsData: {
initialFilterParams,
},
provide,
stubs: {
IssuableList: stubComponent(IssuableList),
},
});
describe('JiraIssuesListRoot', () => {
const resolvedValue = {
headers: { headers: {
'x-page': 1, 'x-page': 1,
'x-total': 3, 'x-total': 3,
}, },
data: mockJiraIssues, data: mockJiraIssues,
}; };
describe('JiraIssuesListRoot', () => {
let wrapper; let wrapper;
let mock; let mock;
const findIssuableList = () => wrapper.find(IssuableList);
const createComponent = ({ provide = mockProvide, initialFilterParams = {} } = {}) => {
wrapper = shallowMount(JiraIssuesListRoot, {
propsData: {
initialFilterParams,
},
provide,
});
};
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = createComponent();
}); });
afterEach(() => { afterEach(() => {
...@@ -52,62 +51,22 @@ describe('JiraIssuesListRoot', () => { ...@@ -52,62 +51,22 @@ describe('JiraIssuesListRoot', () => {
mock.restore(); mock.restore();
}); });
describe('computed', () => { describe('on mount', () => {
describe('showPaginationControls', () => { describe('while loading', () => {
it.each` it('sets issuesListLoading to `true`', async () => {
issuesListLoading | issuesListLoadFailed | issues | totalIssues | returnValue jest.spyOn(axios, 'get').mockResolvedValue(new Promise(() => {}));
${true} | ${false} | ${[]} | ${0} | ${false}
${false} | ${true} | ${[]} | ${0} | ${false}
${false} | ${false} | ${mockJiraIssues} | ${mockJiraIssues.length} | ${true}
`(
'returns $returnValue when issuesListLoading is $issuesListLoading, issuesListLoadFailed is $issuesListLoadFailed, issues is $issues and totalIssues is $totalIssues',
({ issuesListLoading, issuesListLoadFailed, issues, totalIssues, returnValue }) => {
wrapper.setData({
issuesListLoading,
issuesListLoadFailed,
issues,
totalIssues,
});
expect(wrapper.vm.showPaginationControls).toBe(returnValue); createComponent();
},
);
});
describe('urlParams', () => {
it('returns object containing `state`, `page`, `sort` and `search` properties', () => {
wrapper.setData({
currentState: 'closed',
currentPage: 2,
sortedBy: 'created_asc',
filterParams: {
search: 'foo',
},
});
expect(wrapper.vm.urlParams).toMatchObject({ await wrapper.vm.$nextTick();
state: 'closed',
page: 2,
sort: 'created_asc',
search: 'foo',
});
});
});
});
describe('methods', () => {
describe('fetchIssues', () => {
it('sets issuesListLoading to true and issuesListLoadFailed to false', () => {
wrapper.vm.fetchIssues();
expect(wrapper.vm.issuesListLoading).toBe(true); const issuableList = findIssuableList();
expect(wrapper.vm.issuesListLoadFailed).toBe(false); expect(issuableList.props('issuablesLoading')).toBe(true);
}); });
it('calls `axios.get` with `issuesFetchPath` and query params', () => { it('calls `axios.get` with `issuesFetchPath` and query params', () => {
jest.spyOn(axios, 'get').mockResolvedValue(resolvedValue); jest.spyOn(axios, 'get');
createComponent();
wrapper.vm.fetchIssues();
expect(axios.get).toHaveBeenCalledWith( expect(axios.get).toHaveBeenCalledWith(
mockProvide.issuesFetchPath, mockProvide.issuesFetchPath,
...@@ -123,17 +82,30 @@ describe('JiraIssuesListRoot', () => { ...@@ -123,17 +82,30 @@ describe('JiraIssuesListRoot', () => {
}), }),
); );
}); });
});
it('sets `currentPage` and `totalIssues` from response headers and `issues` & `issuesCount` from response body when request is successful', async () => { describe('when request succeeds', () => {
beforeEach(async () => {
jest.spyOn(axios, 'get').mockResolvedValue(resolvedValue); jest.spyOn(axios, 'get').mockResolvedValue(resolvedValue);
await wrapper.vm.fetchIssues(); createComponent();
await waitForPromises();
});
it('sets `currentPage` and `totalIssues` from response headers and `issues` & `issuesCount` from response body when request is successful', async () => {
const issuableList = findIssuableList();
const issuablesProp = issuableList.props('issuables');
const firstIssue = convertObjectPropsToCamelCase(mockJiraIssues[0], { deep: true }); expect(issuableList.props()).toMatchObject({
currentPage: resolvedValue.headers['x-page'],
previousPage: resolvedValue.headers['x-page'] - 1,
nextPage: resolvedValue.headers['x-page'] + 1,
totalItems: resolvedValue.headers['x-total'],
});
expect(issuablesProp).toHaveLength(mockJiraIssues.length);
expect(wrapper.vm.currentPage).toBe(resolvedValue.headers['x-page']); const firstIssue = convertObjectPropsToCamelCase(mockJiraIssues[0], { deep: true });
expect(wrapper.vm.totalIssues).toBe(resolvedValue.headers['x-total']); expect(issuablesProp[0]).toEqual({
expect(wrapper.vm.issues[0]).toEqual({
...firstIssue, ...firstIssue,
id: 31596, id: 31596,
author: { author: {
...@@ -141,127 +113,181 @@ describe('JiraIssuesListRoot', () => { ...@@ -141,127 +113,181 @@ describe('JiraIssuesListRoot', () => {
id: 0, id: 0,
}, },
}); });
expect(wrapper.vm.issuesCount[IssuableStates.Opened]).toBe(3);
}); });
it('sets `issuesListLoadFailed` to true and calls `createFlash` when request fails', async () => { it('sets issuesListLoading to `false`', () => {
jest.spyOn(axios, 'get').mockRejectedValue({}); const issuableList = findIssuableList();
expect(issuableList.props('issuablesLoading')).toBe(false);
});
});
describe('when request fails', () => {
it.each`
APIErrorMessage | expectedRenderedErrorMessage
${'API error'} | ${'An error occurred while loading issues'}
${undefined} | ${'An error occurred while loading issues'}
`(
'calls `createFlash` with "$expectedRenderedErrorMessage" when API responds with "$APIErrorMessage"',
async ({ APIErrorMessage, expectedRenderedErrorMessage }) => {
jest.spyOn(axios, 'get');
mock
.onGet(mockProvide.issuesFetchPath)
.replyOnce(httpStatus.INTERNAL_SERVER_ERROR, { errors: [APIErrorMessage] });
createComponent();
await wrapper.vm.fetchIssues(); await waitForPromises();
expect(wrapper.vm.issuesListLoadFailed).toBe(true);
expect(createFlash).toHaveBeenCalledWith({ expect(createFlash).toHaveBeenCalledWith({
message: 'An error occurred while loading issues', message: expectedRenderedErrorMessage,
captureError: true, captureError: true,
error: expect.any(Object), error: expect.any(Object),
}); });
},
);
});
}); });
it('sets `issuesListLoading` to false when request completes', async () => { it('renders issuable-list component with correct props', async () => {
jest.spyOn(axios, 'get').mockRejectedValue({}); createComponent({ initialFilterParams: { search: 'foo' } });
await wrapper.vm.fetchIssues(); await waitForPromises();
expect(wrapper.vm.issuesListLoading).toBe(false); const issuableList = findIssuableList();
expect(issuableList.exists()).toBe(true);
expect(issuableList.props()).toMatchSnapshot();
}); });
describe('issuable-list events', () => {
beforeEach(async () => {
jest.spyOn(axios, 'get');
createComponent();
await waitForPromises();
}); });
describe('fetchIssuesBy', () => { it('"click-tab" event executes GET request correctly', async () => {
it('sets provided prop value for given prop name and calls `fetchIssues`', () => { const issuableList = findIssuableList();
jest.spyOn(wrapper.vm, 'fetchIssues');
wrapper.vm.fetchIssuesBy('currentPage', 2); issuableList.vm.$emit('click-tab', 'closed');
await waitForPromises();
expect(wrapper.vm.currentPage).toBe(2); expect(axios.get).toHaveBeenCalledWith(mockProvide.issuesFetchPath, {
expect(wrapper.vm.fetchIssues).toHaveBeenCalled(); params: {
}); labels: undefined,
page: 1,
per_page: 2,
search: undefined,
sort: 'created_desc',
state: 'closed',
with_labels_details: true,
},
}); });
expect(issuableList.props('currentTab')).toBe('closed');
}); });
describe('template', () => { it('"page-change" event executes GET request correctly', async () => {
const getIssuableList = () => wrapper.find(IssuableList); const mockPage = 2;
const issuableList = findIssuableList();
it('renders issuable-list component', async () => {
wrapper.setData({
filterParams: {
search: 'foo',
},
});
await wrapper.vm.$nextTick(); issuableList.vm.$emit('page-change', mockPage);
await waitForPromises();
expect(getIssuableList().exists()).toBe(true); expect(axios.get).toHaveBeenCalledWith(mockProvide.issuesFetchPath, {
expect(getIssuableList().props()).toMatchObject({ params: {
namespace: mockProvide.projectFullPath, labels: undefined,
tabs: IssuableListTabs, page: mockPage,
currentTab: 'opened', per_page: 2,
searchInputPlaceholder: 'Search Jira issues', search: undefined,
searchTokens: [], sort: 'created_desc',
sortOptions: AvailableSortOptions, state: 'opened',
initialFilterValue: [ with_labels_details: true,
{
type: 'filtered-search-term',
value: {
data: 'foo',
},
}, },
],
initialSortBy: 'created_desc',
issuables: [],
issuablesLoading: true,
showPaginationControls: wrapper.vm.showPaginationControls,
defaultPageSize: 2, // mocked value in tests
totalItems: 0,
currentPage: 1,
previousPage: 0,
nextPage: 2,
urlParams: wrapper.vm.urlParams,
recentSearchesStorageKey: 'jira_issues',
enableLabelPermalinks: true,
}); });
expect(issuableList.props()).toMatchObject({
currentPage: mockPage,
previousPage: mockPage - 1,
nextPage: mockPage + 1,
}); });
describe('issuable-list events', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm, 'fetchIssues');
}); });
it('click-tab event changes currentState value and calls fetchIssues via `fetchIssuesBy`', () => { it('"sort" event executes GET request correctly', async () => {
getIssuableList().vm.$emit('click-tab', 'closed'); const mockSortBy = 'updated_asc';
const issuableList = findIssuableList();
expect(wrapper.vm.currentState).toBe('closed'); issuableList.vm.$emit('sort', mockSortBy);
expect(wrapper.vm.fetchIssues).toHaveBeenCalled(); await waitForPromises();
});
it('page-change event changes currentPage value and calls fetchIssues via `fetchIssuesBy`', () => { expect(axios.get).toHaveBeenCalledWith(mockProvide.issuesFetchPath, {
getIssuableList().vm.$emit('page-change', 2); params: {
labels: undefined,
expect(wrapper.vm.currentPage).toBe(2); page: 1,
expect(wrapper.vm.fetchIssues).toHaveBeenCalled(); per_page: 2,
search: undefined,
sort: 'created_desc',
state: 'opened',
with_labels_details: true,
},
}); });
expect(issuableList.props('initialSortBy')).toBe(mockSortBy);
it('sort event changes sortedBy value and calls fetchIssues via `fetchIssuesBy`', () => {
getIssuableList().vm.$emit('sort', 'updated_asc');
expect(wrapper.vm.sortedBy).toBe('updated_asc');
expect(wrapper.vm.fetchIssues).toHaveBeenCalled();
}); });
it('filter event sets `filterParams` value and calls fetchIssues', () => { it('filter event sets `filterParams` value and calls fetchIssues', async () => {
getIssuableList().vm.$emit('filter', [ const mockFilterTerm = 'foo';
const issuableList = findIssuableList();
issuableList.vm.$emit('filter', [
{ {
type: 'filtered-search-term', type: 'filtered-search-term',
value: { value: {
data: 'foo', data: mockFilterTerm,
}, },
}, },
]); ]);
await waitForPromises();
expect(wrapper.vm.filterParams).toEqual({ expect(axios.get).toHaveBeenCalledWith(mockProvide.issuesFetchPath, {
search: 'foo', params: {
labels: undefined,
page: 1,
per_page: 2,
search: mockFilterTerm,
sort: 'created_desc',
state: 'opened',
with_labels_details: true,
},
}); });
expect(wrapper.vm.fetchIssues).toHaveBeenCalled();
}); });
}); });
describe('pagination', () => {
it.each`
scenario | issuesListLoadFailed | issues | shouldShowPaginationControls
${'fails'} | ${true} | ${[]} | ${false}
${'returns no issues'} | ${false} | ${[]} | ${false}
${`returns some issues`} | ${false} | ${mockJiraIssues} | ${true}
`(
'sets `showPaginationControls` prop to $shouldShowPaginationControls when request $scenario',
async ({ issuesListLoadFailed, issues, shouldShowPaginationControls }) => {
jest.spyOn(axios, 'get');
mock
.onGet(mockProvide.issuesFetchPath)
.replyOnce(
issuesListLoadFailed ? httpStatus.INTERNAL_SERVER_ERROR : httpStatus.OK,
issues,
{
'x-page': 1,
'x-total': 3,
},
);
createComponent();
await waitForPromises();
expect(findIssuableList().props('showPaginationControls')).toBe(
shouldShowPaginationControls,
);
},
);
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment