Commit 8221fc2e authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '329658-admin-runners-pagination' into 'master'

Add pagination to runners

See merge request gitlab-org/gitlab!62307
parents a323b8b8 801ec7cf
...@@ -113,6 +113,7 @@ export default { ...@@ -113,6 +113,7 @@ export default {
this.$emit('input', { this.$emit('input', {
filters, filters,
sort, sort,
pagination: { page: 1 },
}); });
}, },
onSort(sort) { onSort(sort) {
...@@ -121,6 +122,7 @@ export default { ...@@ -121,6 +122,7 @@ export default {
this.$emit('input', { this.$emit('input', {
filters, filters,
sort, sort,
pagination: { page: 1 },
}); });
}, },
}, },
......
...@@ -136,7 +136,5 @@ export default { ...@@ -136,7 +136,5 @@ export default {
<!-- TODO add actions to update runners --> <!-- TODO add actions to update runners -->
</template> </template>
</gl-table> </gl-table>
<!-- TODO implement pagination -->
</div> </div>
</template> </template>
<script>
import { GlPagination } from '@gitlab/ui';
export default {
components: {
GlPagination,
},
props: {
value: {
required: false,
type: Object,
default: () => ({
page: 1,
}),
},
pageInfo: {
required: false,
type: Object,
default: () => ({}),
},
},
computed: {
prevPage() {
return this.pageInfo?.hasPreviousPage ? this.value?.page - 1 : null;
},
nextPage() {
return this.pageInfo?.hasNextPage ? this.value?.page + 1 : null;
},
},
methods: {
handlePageChange(page) {
if (page > this.value.page) {
this.$emit('input', {
page,
after: this.pageInfo.endCursor,
});
} else {
this.$emit('input', {
page,
before: this.pageInfo.startCursor,
});
}
},
},
};
</script>
<template>
<gl-pagination
:value="value.page"
:prev-page="prevPage"
:next-page="nextPage"
align="center"
class="gl-pagination gl-mt-3"
@input="handlePageChange"
/>
</template>
import { s__ } from '~/locale'; import { s__ } from '~/locale';
export const RUNNER_PAGE_SIZE = 20;
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}'); export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
export const RUNNER_ENTITY_TYPE = 'Ci::Runner'; export const RUNNER_ENTITY_TYPE = 'Ci::Runner';
...@@ -11,6 +13,9 @@ export const RUNNER_ENTITY_TYPE = 'Ci::Runner'; ...@@ -11,6 +13,9 @@ export const RUNNER_ENTITY_TYPE = 'Ci::Runner';
export const PARAM_KEY_STATUS = 'status'; export const PARAM_KEY_STATUS = 'status';
export const PARAM_KEY_RUNNER_TYPE = 'runner_type'; export const PARAM_KEY_RUNNER_TYPE = 'runner_type';
export const PARAM_KEY_SORT = 'sort'; export const PARAM_KEY_SORT = 'sort';
export const PARAM_KEY_PAGE = 'page';
export const PARAM_KEY_AFTER = 'after';
export const PARAM_KEY_BEFORE = 'before';
// CiRunnerType // CiRunnerType
......
query getRunners($status: CiRunnerStatus, $type: CiRunnerType, $sort: CiRunnerSort) { #import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
runners(status: $status, type: $type, sort: $sort) {
query getRunners(
$before: String
$after: String
$first: Int
$last: Int
$status: CiRunnerStatus
$type: CiRunnerType
$sort: CiRunnerSort
) {
runners(
before: $before
after: $after
first: $first
last: $last
status: $status
type: $type
sort: $sort
) {
nodes { nodes {
id id
description description
...@@ -13,5 +31,8 @@ query getRunners($status: CiRunnerStatus, $type: CiRunnerType, $sort: CiRunnerSo ...@@ -13,5 +31,8 @@ query getRunners($status: CiRunnerStatus, $type: CiRunnerType, $sort: CiRunnerSo
tagList tagList
contactedAt contactedAt
} }
pageInfo {
...PageInfo
}
} }
} }
...@@ -3,7 +3,11 @@ import { ...@@ -3,7 +3,11 @@ import {
PARAM_KEY_STATUS, PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE, PARAM_KEY_RUNNER_TYPE,
PARAM_KEY_SORT, PARAM_KEY_SORT,
PARAM_KEY_PAGE,
PARAM_KEY_AFTER,
PARAM_KEY_BEFORE,
DEFAULT_SORT, DEFAULT_SORT,
RUNNER_PAGE_SIZE,
} from '../constants'; } from '../constants';
const getValuesFromFilters = (paramKey, filters) => { const getValuesFromFilters = (paramKey, filters) => {
...@@ -30,6 +34,23 @@ const getFilterFromParams = (paramKey, params) => { ...@@ -30,6 +34,23 @@ const getFilterFromParams = (paramKey, params) => {
}); });
}; };
const getPaginationFromParams = (params) => {
const page = parseInt(params[PARAM_KEY_PAGE], 10);
const after = params[PARAM_KEY_AFTER];
const before = params[PARAM_KEY_BEFORE];
if (page && (before || after)) {
return {
page,
before,
after,
};
}
return {
page: 1,
};
};
export const fromUrlQueryToSearch = (query = window.location.search) => { export const fromUrlQueryToSearch = (query = window.location.search) => {
const params = queryToObject(query, { gatherArrays: true }); const params = queryToObject(query, { gatherArrays: true });
...@@ -39,10 +60,14 @@ export const fromUrlQueryToSearch = (query = window.location.search) => { ...@@ -39,10 +60,14 @@ export const fromUrlQueryToSearch = (query = window.location.search) => {
...getFilterFromParams(PARAM_KEY_RUNNER_TYPE, params), ...getFilterFromParams(PARAM_KEY_RUNNER_TYPE, params),
], ],
sort: params[PARAM_KEY_SORT] || DEFAULT_SORT, sort: params[PARAM_KEY_SORT] || DEFAULT_SORT,
pagination: getPaginationFromParams(params),
}; };
}; };
export const fromSearchToUrl = ({ filters = [], sort = null }, url = window.location.href) => { export const fromSearchToUrl = (
{ filters = [], sort = null, pagination = {} },
url = window.location.href,
) => {
const urlParams = { const urlParams = {
[PARAM_KEY_STATUS]: getValuesFromFilters(PARAM_KEY_STATUS, filters), [PARAM_KEY_STATUS]: getValuesFromFilters(PARAM_KEY_STATUS, filters),
[PARAM_KEY_RUNNER_TYPE]: getValuesFromFilters(PARAM_KEY_RUNNER_TYPE, filters), [PARAM_KEY_RUNNER_TYPE]: getValuesFromFilters(PARAM_KEY_RUNNER_TYPE, filters),
...@@ -52,10 +77,21 @@ export const fromSearchToUrl = ({ filters = [], sort = null }, url = window.loca ...@@ -52,10 +77,21 @@ export const fromSearchToUrl = ({ filters = [], sort = null }, url = window.loca
urlParams[PARAM_KEY_SORT] = sort; urlParams[PARAM_KEY_SORT] = sort;
} }
// Remove pagination params for first page
if (pagination?.page === 1) {
urlParams[PARAM_KEY_PAGE] = null;
urlParams[PARAM_KEY_BEFORE] = null;
urlParams[PARAM_KEY_AFTER] = null;
} else {
urlParams[PARAM_KEY_PAGE] = pagination.page;
urlParams[PARAM_KEY_BEFORE] = pagination.before;
urlParams[PARAM_KEY_AFTER] = pagination.after;
}
return setUrlParams(urlParams, url, false, true, true); return setUrlParams(urlParams, url, false, true, true);
}; };
export const fromSearchToVariables = ({ filters = [], sort = null } = {}) => { export const fromSearchToVariables = ({ filters = [], sort = null, pagination = {} } = {}) => {
const variables = {}; const variables = {};
// TODO Get more than one value when GraphQL API supports OR for "status" // TODO Get more than one value when GraphQL API supports OR for "status"
...@@ -68,5 +104,13 @@ export const fromSearchToVariables = ({ filters = [], sort = null } = {}) => { ...@@ -68,5 +104,13 @@ export const fromSearchToVariables = ({ filters = [], sort = null } = {}) => {
variables.sort = sort; variables.sort = sort;
} }
if (pagination.before) {
variables.before = pagination.before;
variables.last = RUNNER_PAGE_SIZE;
} else {
variables.after = pagination.after;
variables.first = RUNNER_PAGE_SIZE;
}
return variables; return variables;
}; };
...@@ -4,6 +4,7 @@ import { updateHistory } from '~/lib/utils/url_utility'; ...@@ -4,6 +4,7 @@ import { updateHistory } from '~/lib/utils/url_utility';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue'; import RunnerList from '../components/runner_list.vue';
import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue'; import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue'; import RunnerTypeHelp from '../components/runner_type_help.vue';
import getRunnersQuery from '../graphql/get_runners.query.graphql'; import getRunnersQuery from '../graphql/get_runners.query.graphql';
import { import {
...@@ -18,6 +19,7 @@ export default { ...@@ -18,6 +19,7 @@ export default {
RunnerList, RunnerList,
RunnerManualSetupHelp, RunnerManualSetupHelp,
RunnerTypeHelp, RunnerTypeHelp,
RunnerPagination,
}, },
props: { props: {
activeRunnersCount: { activeRunnersCount: {
...@@ -32,7 +34,10 @@ export default { ...@@ -32,7 +34,10 @@ export default {
data() { data() {
return { return {
search: fromUrlQueryToSearch(), search: fromUrlQueryToSearch(),
runners: [], runners: {
items: [],
pageInfo: {},
},
}; };
}, },
apollo: { apollo: {
...@@ -41,8 +46,12 @@ export default { ...@@ -41,8 +46,12 @@ export default {
variables() { variables() {
return this.variables; return this.variables;
}, },
update({ runners }) { update(data) {
return runners?.nodes || []; const { runners } = data;
return {
items: runners?.nodes || [],
pageInfo: runners?.pageInfo || {},
};
}, },
error(err) { error(err) {
this.captureException(err); this.captureException(err);
...@@ -57,19 +66,21 @@ export default { ...@@ -57,19 +66,21 @@ export default {
return this.$apollo.queries.runners.loading; return this.$apollo.queries.runners.loading;
}, },
noRunnersFound() { noRunnersFound() {
return !this.runnersLoading && !this.runners.length; return !this.runnersLoading && !this.runners.items.length;
}, },
}, },
watch: { watch: {
search() { search: {
deep: true,
handler() {
// TODO Implement back button reponse using onpopstate // TODO Implement back button reponse using onpopstate
updateHistory({ updateHistory({
url: fromSearchToUrl(this.search), url: fromSearchToUrl(this.search),
title: document.title, title: document.title,
}); });
}, },
}, },
},
errorCaptured(err) { errorCaptured(err) {
this.captureException(err); this.captureException(err);
}, },
...@@ -99,11 +110,13 @@ export default { ...@@ -99,11 +110,13 @@ export default {
<div v-if="noRunnersFound" class="gl-text-center gl-p-5"> <div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }} {{ __('No runners found') }}
</div> </div>
<template v-else>
<runner-list <runner-list
v-else :runners="runners.items"
:runners="runners"
:loading="runnersLoading" :loading="runnersLoading"
:active-runners-count="activeRunnersCount" :active-runners-count="activeRunnersCount"
/> />
<runner-pagination v-model="search.pagination" :page-info="runners.pageInfo" />
</template>
</div> </div>
</template> </template>
...@@ -118,6 +118,7 @@ describe('RunnerList', () => { ...@@ -118,6 +118,7 @@ describe('RunnerList', () => {
{ {
filters: mockFilters, filters: mockFilters,
sort: mockDefaultSort, sort: mockDefaultSort,
pagination: { page: 1 },
}, },
]); ]);
}); });
...@@ -129,6 +130,7 @@ describe('RunnerList', () => { ...@@ -129,6 +130,7 @@ describe('RunnerList', () => {
{ {
filters: [], filters: [],
sort: mockOtherSort, sort: mockOtherSort,
pagination: { page: 1 },
}, },
]); ]);
}); });
......
import { GlPagination } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
const mockStartCursor = 'START_CURSOR';
const mockEndCursor = 'END_CURSOR';
describe('RunnerPagination', () => {
let wrapper;
const findPagination = () => wrapper.findComponent(GlPagination);
const createComponent = ({ page = 1, hasPreviousPage = false, hasNextPage = true } = {}) => {
wrapper = mount(RunnerPagination, {
propsData: {
value: {
page,
},
pageInfo: {
hasPreviousPage,
hasNextPage,
startCursor: mockStartCursor,
endCursor: mockEndCursor,
},
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('When on the first page', () => {
beforeEach(() => {
createComponent({
page: 1,
hasPreviousPage: false,
hasNextPage: true,
});
});
it('Contains the current page information', () => {
expect(findPagination().props('value')).toBe(1);
expect(findPagination().props('prevPage')).toBe(null);
expect(findPagination().props('nextPage')).toBe(2);
});
it('Shows prev page disabled', () => {
expect(findPagination().find('[aria-disabled]').text()).toBe('Prev');
});
it('Shows next page link', () => {
expect(findPagination().find('a').text()).toBe('Next');
});
it('Goes to the second page', () => {
findPagination().vm.$emit('input', 2);
expect(wrapper.emitted('input')[0]).toEqual([
{
after: mockEndCursor,
page: 2,
},
]);
});
});
describe('When in between pages', () => {
beforeEach(() => {
createComponent({
page: 2,
hasPreviousPage: true,
hasNextPage: true,
});
});
it('Contains the current page information', () => {
expect(findPagination().props('value')).toBe(2);
expect(findPagination().props('prevPage')).toBe(1);
expect(findPagination().props('nextPage')).toBe(3);
});
it('Shows the next and previous pages', () => {
const links = findPagination().findAll('a');
expect(links).toHaveLength(2);
expect(links.at(0).text()).toBe('Prev');
expect(links.at(1).text()).toBe('Next');
});
it('Goes to the last page', () => {
findPagination().vm.$emit('input', 3);
expect(wrapper.emitted('input')[0]).toEqual([
{
after: mockEndCursor,
page: 3,
},
]);
});
it('Goes to the first page', () => {
findPagination().vm.$emit('input', 1);
expect(wrapper.emitted('input')[0]).toEqual([
{
before: mockStartCursor,
page: 1,
},
]);
});
});
describe('When in the last page', () => {
beforeEach(() => {
createComponent({
page: 3,
hasPreviousPage: true,
hasNextPage: false,
});
});
it('Contains the current page', () => {
expect(findPagination().props('value')).toBe(3);
expect(findPagination().props('prevPage')).toBe(2);
expect(findPagination().props('nextPage')).toBe(null);
});
it('Shows next page link', () => {
expect(findPagination().find('a').text()).toBe('Prev');
});
it('Shows next page disabled', () => {
expect(findPagination().find('[aria-disabled]').text()).toBe('Next');
});
});
describe('When only one page', () => {
beforeEach(() => {
createComponent({
page: 1,
hasPreviousPage: false,
hasNextPage: false,
});
});
it('does not display pagination', () => {
expect(wrapper.html()).toBe('');
});
it('Contains the current page', () => {
expect(findPagination().props('value')).toBe(1);
});
it('Shows no more page buttons', () => {
expect(findPagination().props('prevPage')).toBe(null);
expect(findPagination().props('nextPage')).toBe(null);
});
});
});
...@@ -31,6 +31,13 @@ export const runnersData = { ...@@ -31,6 +31,13 @@ export const runnersData = {
__typename: 'CiRunner', __typename: 'CiRunner',
}, },
], ],
pageInfo: {
endCursor: 'GRAPHQL_END_CURSOR',
startCursor: 'GRAPHQL_START_CURSOR',
hasNextPage: true,
hasPreviousPage: false,
__typename: 'PageInfo',
},
__typename: 'CiRunnerConnection', __typename: 'CiRunnerConnection',
}, },
}, },
......
import { RUNNER_PAGE_SIZE } from '~/runner/constants';
import { import {
fromUrlQueryToSearch, fromUrlQueryToSearch,
fromSearchToUrl, fromSearchToUrl,
...@@ -9,26 +10,28 @@ describe('search_params.js', () => { ...@@ -9,26 +10,28 @@ describe('search_params.js', () => {
{ {
name: 'a default query', name: 'a default query',
urlQuery: '', urlQuery: '',
search: { filters: [], sort: 'CREATED_DESC' }, search: { filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' },
graphqlVariables: { sort: 'CREATED_DESC' }, graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
}, },
{ {
name: 'a single status', name: 'a single status',
urlQuery: '?status[]=ACTIVE', urlQuery: '?status[]=ACTIVE',
search: { search: {
filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }], filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
pagination: { page: 1 },
sort: 'CREATED_DESC', sort: 'CREATED_DESC',
}, },
graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC' }, graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
}, },
{ {
name: 'single instance type', name: 'single instance type',
urlQuery: '?runner_type[]=INSTANCE_TYPE', urlQuery: '?runner_type[]=INSTANCE_TYPE',
search: { search: {
filters: [{ type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } }], filters: [{ type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } }],
pagination: { page: 1 },
sort: 'CREATED_DESC', sort: 'CREATED_DESC',
}, },
graphqlVariables: { type: 'INSTANCE_TYPE', sort: 'CREATED_DESC' }, graphqlVariables: { type: 'INSTANCE_TYPE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
}, },
{ {
name: 'multiple runner status', name: 'multiple runner status',
...@@ -38,9 +41,10 @@ describe('search_params.js', () => { ...@@ -38,9 +41,10 @@ describe('search_params.js', () => {
{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }, { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
{ type: 'status', value: { data: 'PAUSED', operator: '=' } }, { type: 'status', value: { data: 'PAUSED', operator: '=' } },
], ],
pagination: { page: 1 },
sort: 'CREATED_DESC', sort: 'CREATED_DESC',
}, },
graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC' }, graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
}, },
{ {
name: 'multiple status, a single instance type and a non default sort', name: 'multiple status, a single instance type and a non default sort',
...@@ -50,9 +54,52 @@ describe('search_params.js', () => { ...@@ -50,9 +54,52 @@ describe('search_params.js', () => {
{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }, { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
{ type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } }, { type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } },
], ],
pagination: { page: 1 },
sort: 'CREATED_ASC', sort: 'CREATED_ASC',
}, },
graphqlVariables: { status: 'ACTIVE', type: 'INSTANCE_TYPE', sort: 'CREATED_ASC' }, graphqlVariables: {
status: 'ACTIVE',
type: 'INSTANCE_TYPE',
sort: 'CREATED_ASC',
first: RUNNER_PAGE_SIZE,
},
},
{
name: 'the next page',
urlQuery: '?page=2&after=AFTER_CURSOR',
search: { filters: [], pagination: { page: 2, after: 'AFTER_CURSOR' }, sort: 'CREATED_DESC' },
graphqlVariables: { sort: 'CREATED_DESC', after: 'AFTER_CURSOR', first: RUNNER_PAGE_SIZE },
},
{
name: 'the previous page',
urlQuery: '?page=2&before=BEFORE_CURSOR',
search: {
filters: [],
pagination: { page: 2, before: 'BEFORE_CURSOR' },
sort: 'CREATED_DESC',
},
graphqlVariables: { sort: 'CREATED_DESC', before: 'BEFORE_CURSOR', last: RUNNER_PAGE_SIZE },
},
{
name:
'the next page filtered by multiple status, a single instance type and a non default sort',
urlQuery:
'?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC&page=2&after=AFTER_CURSOR',
search: {
filters: [
{ type: 'status', value: { data: 'ACTIVE', operator: '=' } },
{ type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } },
],
pagination: { page: 2, after: 'AFTER_CURSOR' },
sort: 'CREATED_ASC',
},
graphqlVariables: {
status: 'ACTIVE',
type: 'INSTANCE_TYPE',
sort: 'CREATED_ASC',
after: 'AFTER_CURSOR',
first: RUNNER_PAGE_SIZE,
},
}, },
]; ];
...@@ -62,6 +109,24 @@ describe('search_params.js', () => { ...@@ -62,6 +109,24 @@ describe('search_params.js', () => {
expect(fromUrlQueryToSearch(urlQuery)).toEqual(search); expect(fromUrlQueryToSearch(urlQuery)).toEqual(search);
}); });
}); });
it('When a page cannot be parsed as a number, it defaults to `1`', () => {
expect(fromUrlQueryToSearch('?page=NONSENSE&after=AFTER_CURSOR').pagination).toEqual({
page: 1,
});
});
it('When a page is less than 1, it defaults to `1`', () => {
expect(fromUrlQueryToSearch('?page=0&after=AFTER_CURSOR').pagination).toEqual({
page: 1,
});
});
it('When a page with no cursor is given, it defaults to `1`', () => {
expect(fromUrlQueryToSearch('?page=2').pagination).toEqual({
page: 1,
});
});
}); });
describe('fromSearchToUrl', () => { describe('fromSearchToUrl', () => {
......
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
...@@ -9,14 +9,17 @@ import { updateHistory } from '~/lib/utils/url_utility'; ...@@ -9,14 +9,17 @@ import { updateHistory } from '~/lib/utils/url_utility';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue'; import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue'; import RunnerList from '~/runner/components/runner_list.vue';
import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue'; import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
import RunnerTypeHelp from '~/runner/components/runner_type_help.vue'; import RunnerTypeHelp from '~/runner/components/runner_type_help.vue';
import { import {
CREATED_ASC, CREATED_ASC,
CREATED_DESC,
DEFAULT_SORT, DEFAULT_SORT,
INSTANCE_TYPE, INSTANCE_TYPE,
PARAM_KEY_STATUS, PARAM_KEY_STATUS,
STATUS_ACTIVE, STATUS_ACTIVE,
RUNNER_PAGE_SIZE,
} from '~/runner/constants'; } from '~/runner/constants';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql'; import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
import RunnerListApp from '~/runner/runner_list/runner_list_app.vue'; import RunnerListApp from '~/runner/runner_list/runner_list_app.vue';
...@@ -26,6 +29,7 @@ import { runnersData } from '../mock_data'; ...@@ -26,6 +29,7 @@ import { runnersData } from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN'; const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
const mockActiveRunnersCount = 2; const mockActiveRunnersCount = 2;
const mocKRunners = runnersData.data.runners.nodes; const mocKRunners = runnersData.data.runners.nodes;
const mockPageInfo = runnersData.data.runners.pageInfo;
jest.mock('@sentry/browser'); jest.mock('@sentry/browser');
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
...@@ -44,6 +48,7 @@ describe('RunnerListApp', () => { ...@@ -44,6 +48,7 @@ describe('RunnerListApp', () => {
const findRunnerTypeHelp = () => wrapper.findComponent(RunnerTypeHelp); const findRunnerTypeHelp = () => wrapper.findComponent(RunnerTypeHelp);
const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp); const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp);
const findRunnerList = () => wrapper.findComponent(RunnerList); const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => wrapper.findComponent(RunnerPagination);
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar); const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => { const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
...@@ -101,6 +106,7 @@ describe('RunnerListApp', () => { ...@@ -101,6 +106,7 @@ describe('RunnerListApp', () => {
status: undefined, status: undefined,
type: undefined, type: undefined,
sort: DEFAULT_SORT, sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
}); });
}); });
...@@ -128,6 +134,7 @@ describe('RunnerListApp', () => { ...@@ -128,6 +134,7 @@ describe('RunnerListApp', () => {
{ type: 'runner_type', value: { data: INSTANCE_TYPE, operator: '=' } }, { type: 'runner_type', value: { data: INSTANCE_TYPE, operator: '=' } },
], ],
sort: 'CREATED_DESC', sort: 'CREATED_DESC',
pagination: { page: 1 },
}); });
}); });
...@@ -136,6 +143,7 @@ describe('RunnerListApp', () => { ...@@ -136,6 +143,7 @@ describe('RunnerListApp', () => {
status: STATUS_ACTIVE, status: STATUS_ACTIVE,
type: INSTANCE_TYPE, type: INSTANCE_TYPE,
sort: DEFAULT_SORT, sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
}); });
}); });
}); });
...@@ -159,6 +167,7 @@ describe('RunnerListApp', () => { ...@@ -159,6 +167,7 @@ describe('RunnerListApp', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({ expect(mockRunnersQuery).toHaveBeenLastCalledWith({
status: STATUS_ACTIVE, status: STATUS_ACTIVE,
sort: CREATED_ASC, sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
}); });
}); });
}); });
...@@ -193,4 +202,37 @@ describe('RunnerListApp', () => { ...@@ -193,4 +202,37 @@ describe('RunnerListApp', () => {
expect(Sentry.captureException).toHaveBeenCalled(); expect(Sentry.captureException).toHaveBeenCalled();
}); });
}); });
describe('Pagination', () => {
beforeEach(() => {
createComponentWithApollo({ mountFn: mount });
});
it('more pages can be selected', () => {
expect(findRunnerPagination().text()).toMatchInterpolatedText('Prev Next');
});
it('cannot navigate to the previous page', () => {
expect(findRunnerPagination().find('[aria-disabled]').text()).toBe('Prev');
});
it('navigates to the next page', async () => {
const nextPageBtn = findRunnerPagination().find('a');
expect(nextPageBtn.text()).toBe('Next');
await nextPageBtn.trigger('click');
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
after: expect.any(String),
});
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
after: mockPageInfo.endCursor,
});
});
});
}); });
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