Commit 82d51739 authored by Nathan Friend's avatar Nathan Friend Committed by Nicolò Maria Mezzopera

Add GraphQL pagination to Releases page

This commit updates the Releases page to include pagination when in
"GraphQL mode".

Because this is the last step in the GraphQL feature, this MR also
enables the `graphql_releases_page` feature flag by default.
parent 83ccc18d
......@@ -6,14 +6,10 @@ import {
GlLink,
GlButton,
} from '@gitlab/ui';
import {
getParameterByName,
historyPushState,
buildUrlWithCurrentLocation,
} from '~/lib/utils/common_utils';
import { getParameterByName } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import ReleaseBlock from './release_block.vue';
import ReleasesPagination from './releases_pagination.vue';
export default {
name: 'ReleasesApp',
......@@ -21,7 +17,7 @@ export default {
GlSkeletonLoading,
GlEmptyState,
ReleaseBlock,
TablePagination,
ReleasesPagination,
GlLink,
GlButton,
},
......@@ -33,7 +29,6 @@ export default {
'isLoading',
'releases',
'hasError',
'pageInfo',
]),
shouldRenderEmptyState() {
return !this.releases.length && !this.hasError && !this.isLoading;
......@@ -48,15 +43,23 @@ export default {
},
},
created() {
this.fetchReleases({
page: getParameterByName('page'),
});
this.fetchReleases();
window.addEventListener('popstate', this.fetchReleases);
},
methods: {
...mapActions('list', ['fetchReleases']),
onChangePage(page) {
historyPushState(buildUrlWithCurrentLocation(`?page=${page}`));
this.fetchReleases({ page });
...mapActions('list', {
fetchReleasesStoreAction: 'fetchReleases',
}),
fetchReleases() {
this.fetchReleasesStoreAction({
// these two parameters are only used in "GraphQL mode"
before: getParameterByName('before'),
after: getParameterByName('after'),
// this parameter is only used when in "REST mode"
page: getParameterByName('page'),
});
},
},
};
......@@ -105,7 +108,7 @@ export default {
/>
</div>
<table-pagination v-if="!isLoading" :change="onChangePage" :page-info="pageInfo" />
<releases-pagination v-if="!isLoading" />
</div>
</template>
<style>
......
......@@ -13,14 +13,14 @@ export default {
},
},
methods: {
...mapActions('list', ['fetchReleasesGraphQl']),
...mapActions('list', ['fetchReleases']),
onPrev(before) {
historyPushState(buildUrlWithCurrentLocation(`?before=${before}`));
this.fetchReleasesGraphQl({ before });
this.fetchReleases({ before });
},
onNext(after) {
historyPushState(buildUrlWithCurrentLocation(`?after=${after}`));
this.fetchReleasesGraphQl({ after });
this.fetchReleases({ after });
},
},
};
......
......@@ -7,18 +7,18 @@ export default {
name: 'ReleasesPaginationRest',
components: { TablePagination },
computed: {
...mapState('list', ['pageInfo']),
...mapState('list', ['restPageInfo']),
},
methods: {
...mapActions('list', ['fetchReleasesRest']),
...mapActions('list', ['fetchReleases']),
onChangePage(page) {
historyPushState(buildUrlWithCurrentLocation(`?page=${page}`));
this.fetchReleasesRest({ page });
this.fetchReleases({ page });
},
},
};
</script>
<template>
<table-pagination :change="onChangePage" :page-info="pageInfo" />
<table-pagination :change="onChangePage" :page-info="restPageInfo" />
</template>
......@@ -10,3 +10,5 @@ export const ASSET_LINK_TYPE = Object.freeze({
});
export const DEFAULT_ASSET_LINK_TYPE = ASSET_LINK_TYPE.OTHER;
export const PAGE_SIZE = 20;
query allReleases($fullPath: ID!) {
query allReleases($fullPath: ID!, $first: Int, $last: Int, $before: String, $after: String) {
project(fullPath: $fullPath) {
releases(first: 20) {
count
releases(first: $first, last: $last, before: $before, after: $after) {
nodes {
name
tagName
......@@ -64,6 +63,12 @@ query allReleases($fullPath: ID!) {
}
}
}
pageInfo {
startCursor
hasPreviousPage
hasNextPage
endCursor
}
}
}
}
/**
* @returns {Boolean} `true` if all the feature flags
* required to enable the GraphQL endpoint are enabled
*/
export const useGraphQLEndpoint = rootState => {
return Boolean(
rootState.featureFlags.graphqlReleaseData &&
rootState.featureFlags.graphqlReleasesPage &&
rootState.featureFlags.graphqlMilestoneStats,
);
};
import Vuex from 'vuex';
import * as getters from './getters';
export default ({ modules, featureFlags }) =>
new Vuex.Store({
modules,
state: { featureFlags },
getters,
});
......@@ -9,54 +9,89 @@ import {
} from '~/lib/utils/common_utils';
import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
import { gqClient, convertGraphQLResponse } from '../../../util';
import { PAGE_SIZE } from '../../../constants';
/**
* Commits a mutation to update the state while the main endpoint is being requested.
* Gets a paginated list of releases from the server
*
* @param {Object} vuexParams
* @param {Object} actionParams
* @param {Number} [actionParams.page] The page number of results to fetch
* (this parameter is only used when fetching results from the REST API)
* @param {String} [actionParams.before] A GraphQL cursor. If provided,
* the items returned will proceed the provided cursor (this parameter is only
* used when fetching results from the GraphQL API).
* @param {String} [actionParams.after] A GraphQL cursor. If provided,
* the items returned will follow the provided cursor (this parameter is only
* used when fetching results from the GraphQL API).
*/
export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES);
export const fetchReleases = ({ dispatch, rootGetters }, { page = 1, before, after }) => {
if (rootGetters.useGraphQLEndpoint) {
dispatch('fetchReleasesGraphQl', { before, after });
} else {
dispatch('fetchReleasesRest', { page });
}
};
/**
* Fetches the main endpoint.
* Will dispatch requestNamespace action before starting the request.
* Will dispatch receiveNamespaceSuccess if the request is successful
* Will dispatch receiveNamesapceError if the request returns an error
*
* @param {String} projectId
* Gets a paginated list of releases from the GraphQL endpoint
*/
export const fetchReleases = ({ dispatch, rootState, state }, { page = '1' }) => {
dispatch('requestReleases');
export const fetchReleasesGraphQl = (
{ dispatch, commit, state },
{ before = null, after = null },
) => {
commit(types.REQUEST_RELEASES);
if (
rootState.featureFlags.graphqlReleaseData &&
rootState.featureFlags.graphqlReleasesPage &&
rootState.featureFlags.graphqlMilestoneStats
) {
gqClient
.query({
query: allReleasesQuery,
variables: {
fullPath: state.projectPath,
},
})
.then(response => {
dispatch('receiveReleasesSuccess', convertGraphQLResponse(response));
})
.catch(() => dispatch('receiveReleasesError'));
let paginationParams;
if (!before && !after) {
paginationParams = { first: PAGE_SIZE };
} else if (before && !after) {
paginationParams = { last: PAGE_SIZE, before };
} else if (!before && after) {
paginationParams = { first: PAGE_SIZE, after };
} else {
api
.releases(state.projectId, { page })
.then(response => dispatch('receiveReleasesSuccess', response))
.catch(() => dispatch('receiveReleasesError'));
throw new Error(
'Both a `before` and an `after` parameter were provided to fetchReleasesGraphQl. These parameters cannot be used together.',
);
}
gqClient
.query({
query: allReleasesQuery,
variables: {
fullPath: state.projectPath,
...paginationParams,
},
})
.then(response => {
const { data, paginationInfo: graphQlPageInfo } = convertGraphQLResponse(response);
commit(types.RECEIVE_RELEASES_SUCCESS, {
data,
graphQlPageInfo,
});
})
.catch(() => dispatch('receiveReleasesError'));
};
export const receiveReleasesSuccess = ({ commit }, { data, headers }) => {
const pageInfo = parseIntPagination(normalizeHeaders(headers));
const camelCasedReleases = convertObjectPropsToCamelCase(data, { deep: true });
commit(types.RECEIVE_RELEASES_SUCCESS, {
data: camelCasedReleases,
pageInfo,
});
/**
* Gets a paginated list of releases from the REST endpoint
*/
export const fetchReleasesRest = ({ dispatch, commit, state }, { page }) => {
commit(types.REQUEST_RELEASES);
api
.releases(state.projectId, { page })
.then(({ data, headers }) => {
const restPageInfo = parseIntPagination(normalizeHeaders(headers));
const camelCasedReleases = convertObjectPropsToCamelCase(data, { deep: true });
commit(types.RECEIVE_RELEASES_SUCCESS, {
data: camelCasedReleases,
restPageInfo,
});
})
.catch(() => dispatch('receiveReleasesError'));
};
export const receiveReleasesError = ({ commit }) => {
......
......@@ -17,11 +17,12 @@ export default {
* @param {Object} state
* @param {Object} resp
*/
[types.RECEIVE_RELEASES_SUCCESS](state, { data, pageInfo }) {
[types.RECEIVE_RELEASES_SUCCESS](state, { data, restPageInfo, graphQlPageInfo }) {
state.hasError = false;
state.isLoading = false;
state.releases = data;
state.pageInfo = pageInfo;
state.restPageInfo = restPageInfo;
state.graphQlPageInfo = graphQlPageInfo;
},
/**
......@@ -35,5 +36,7 @@ export default {
state.isLoading = false;
state.releases = [];
state.hasError = true;
state.restPageInfo = {};
state.graphQlPageInfo = {};
},
};
......@@ -14,5 +14,6 @@ export default ({
isLoading: false,
hasError: false,
releases: [],
pageInfo: {},
restPageInfo: {},
graphQlPageInfo: {},
});
......@@ -126,5 +126,9 @@ export const convertGraphQLResponse = response => {
...convertMilestones(r),
}));
return { data: releases };
const paginationInfo = {
...response.data.project.releases.pageInfo,
};
return { data: releases, paginationInfo };
};
......@@ -13,7 +13,7 @@ class Projects::ReleasesController < Projects::ApplicationController
push_frontend_feature_flag(:release_asset_link_type, project, default_enabled: true)
push_frontend_feature_flag(:graphql_release_data, project, default_enabled: true)
push_frontend_feature_flag(:graphql_milestone_stats, project, default_enabled: true)
push_frontend_feature_flag(:graphql_releases_page, project, default_enabled: false)
push_frontend_feature_flag(:graphql_releases_page, project, default_enabled: true)
end
before_action :authorize_update_release!, only: %i[edit update]
before_action :authorize_create_release!, only: :new
......
......@@ -109,5 +109,11 @@ Object {
"upcomingRelease": false,
},
],
"paginationInfo": Object {
"endCursor": "eyJpZCI6IjMiLCJyZWxlYXNlZF9hdCI6IjIwMjAtMDctMDkgMjA6MTE6MzMuODA0OTYxMDAwIFVUQyJ9",
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "eyJpZCI6IjQ0IiwicmVsZWFzZWRfYXQiOiIyMDMwLTAzLTE1IDA4OjAwOjAwLjAwMDAwMDAwMCBVVEMifQ",
},
}
`;
......@@ -13,7 +13,14 @@ import {
releases,
} from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import ReleasesPagination from '~/releases/components/releases_pagination.vue';
jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'),
getParameterByName: jest.fn().mockImplementation(paramName => {
return `${paramName}_param_value`;
}),
}));
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -22,7 +29,7 @@ describe('Releases App ', () => {
let wrapper;
let fetchReleaseSpy;
const releasesPagination = rge(21).map(index => ({
const paginatedReleases = rge(21).map(index => ({
...convertObjectPropsToCamelCase(release, { deep: true }),
tagName: `${index}.00`,
}));
......@@ -70,9 +77,13 @@ describe('Releases App ', () => {
createComponent();
});
it('calls fetchRelease with the page parameter', () => {
it('calls fetchRelease with the page, before, and after parameters', () => {
expect(fetchReleaseSpy).toHaveBeenCalledTimes(1);
expect(fetchReleaseSpy).toHaveBeenCalledWith(expect.anything(), { page: null });
expect(fetchReleaseSpy).toHaveBeenCalledWith(expect.anything(), {
page: 'page_param_value',
before: 'before_param_value',
after: 'after_param_value',
});
});
});
......@@ -91,7 +102,7 @@ describe('Releases App ', () => {
expect(wrapper.contains('.js-loading')).toBe(true);
expect(wrapper.contains('.js-empty-state')).toBe(false);
expect(wrapper.contains('.js-success-state')).toBe(false);
expect(wrapper.contains(TablePagination)).toBe(false);
expect(wrapper.contains(ReleasesPagination)).toBe(false);
});
});
......@@ -108,7 +119,7 @@ describe('Releases App ', () => {
expect(wrapper.contains('.js-loading')).toBe(false);
expect(wrapper.contains('.js-empty-state')).toBe(false);
expect(wrapper.contains('.js-success-state')).toBe(true);
expect(wrapper.contains(TablePagination)).toBe(true);
expect(wrapper.contains(ReleasesPagination)).toBe(true);
});
});
......@@ -116,7 +127,7 @@ describe('Releases App ', () => {
beforeEach(() => {
jest
.spyOn(api, 'releases')
.mockResolvedValue({ data: releasesPagination, headers: pageInfoHeadersWithPagination });
.mockResolvedValue({ data: paginatedReleases, headers: pageInfoHeadersWithPagination });
createComponent();
});
......@@ -125,7 +136,7 @@ describe('Releases App ', () => {
expect(wrapper.contains('.js-loading')).toBe(false);
expect(wrapper.contains('.js-empty-state')).toBe(false);
expect(wrapper.contains('.js-success-state')).toBe(true);
expect(wrapper.contains(TablePagination)).toBe(true);
expect(wrapper.contains(ReleasesPagination)).toBe(true);
});
});
......@@ -154,7 +165,7 @@ describe('Releases App ', () => {
const newReleasePath = 'path/to/new/release';
beforeEach(() => {
createComponent({ ...defaultInitialState, newReleasePath });
createComponent({ newReleasePath });
});
it('renders the "New release" button', () => {
......@@ -174,4 +185,27 @@ describe('Releases App ', () => {
});
});
});
describe('when the back button is pressed', () => {
beforeEach(() => {
jest
.spyOn(api, 'releases')
.mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination });
createComponent();
fetchReleaseSpy.mockClear();
window.dispatchEvent(new PopStateEvent('popstate'));
});
it('calls fetchRelease with the page parameter', () => {
expect(fetchReleaseSpy).toHaveBeenCalledTimes(1);
expect(fetchReleaseSpy).toHaveBeenCalledWith(expect.anything(), {
page: 'page_param_value',
before: 'before_param_value',
after: 'after_param_value',
});
});
});
});
......@@ -29,7 +29,7 @@ describe('~/releases/components/releases_pagination_graphql.vue', () => {
listModule.state.graphQlPageInfo = pageInfo;
listModule.actions.fetchReleasesGraphQl = jest.fn();
listModule.actions.fetchReleases = jest.fn();
wrapper = mount(ReleasesPaginationGraphql, {
store: createStore({
......@@ -141,8 +141,8 @@ describe('~/releases/components/releases_pagination_graphql.vue', () => {
findNextButton().trigger('click');
});
it('calls fetchReleasesGraphQl with the correct after cursor', () => {
expect(listModule.actions.fetchReleasesGraphQl.mock.calls).toEqual([
it('calls fetchReleases with the correct after cursor', () => {
expect(listModule.actions.fetchReleases.mock.calls).toEqual([
[expect.anything(), { after: cursors.endCursor }],
]);
});
......@@ -159,8 +159,8 @@ describe('~/releases/components/releases_pagination_graphql.vue', () => {
findPrevButton().trigger('click');
});
it('calls fetchReleasesGraphQl with the correct before cursor', () => {
expect(listModule.actions.fetchReleasesGraphQl.mock.calls).toEqual([
it('calls fetchReleases with the correct before cursor', () => {
expect(listModule.actions.fetchReleases.mock.calls).toEqual([
[expect.anything(), { before: cursors.startCursor }],
]);
});
......
......@@ -20,9 +20,9 @@ describe('~/releases/components/releases_pagination_rest.vue', () => {
const createComponent = pageInfo => {
listModule = createListModule({ projectId });
listModule.state.pageInfo = pageInfo;
listModule.state.restPageInfo = pageInfo;
listModule.actions.fetchReleasesRest = jest.fn();
listModule.actions.fetchReleases = jest.fn();
wrapper = mount(ReleasesPaginationRest, {
store: createStore({
......@@ -57,8 +57,8 @@ describe('~/releases/components/releases_pagination_rest.vue', () => {
findGlPagination().vm.$emit('input', newPage);
});
it('calls fetchReleasesRest with the correct page', () => {
expect(listModule.actions.fetchReleasesRest.mock.calls).toEqual([
it('calls fetchReleases with the correct page', () => {
expect(listModule.actions.fetchReleases.mock.calls).toEqual([
[expect.anything(), { page: newPage }],
]);
});
......
......@@ -346,6 +346,14 @@ export const graphqlReleasesResponse = {
},
},
],
pageInfo: {
startCursor:
'eyJpZCI6IjQ0IiwicmVsZWFzZWRfYXQiOiIyMDMwLTAzLTE1IDA4OjAwOjAwLjAwMDAwMDAwMCBVVEMifQ',
hasPreviousPage: false,
hasNextPage: true,
endCursor:
'eyJpZCI6IjMiLCJyZWxlYXNlZF9hdCI6IjIwMjAtMDctMDkgMjA6MTE6MzMuODA0OTYxMDAwIFVUQyJ9',
},
},
},
},
......
import * as getters from '~/releases/stores/getters';
describe('~/releases/stores/getters.js', () => {
it.each`
graphqlReleaseData | graphqlReleasesPage | graphqlMilestoneStats | result
${false} | ${false} | ${false} | ${false}
${false} | ${false} | ${true} | ${false}
${false} | ${true} | ${false} | ${false}
${false} | ${true} | ${true} | ${false}
${true} | ${false} | ${false} | ${false}
${true} | ${false} | ${true} | ${false}
${true} | ${true} | ${false} | ${false}
${true} | ${true} | ${true} | ${true}
`(
'returns $result with feature flag values graphqlReleaseData=$graphqlReleaseData, graphqlReleasesPage=$graphqlReleasesPage, and graphqlMilestoneStats=$graphqlMilestoneStats',
({ result: expectedResult, ...featureFlags }) => {
const actualResult = getters.useGraphQLEndpoint({ featureFlags });
expect(actualResult).toBe(expectedResult);
},
);
});
import { cloneDeep } from 'lodash';
import testAction from 'helpers/vuex_action_helper';
import {
requestReleases,
fetchReleases,
receiveReleasesSuccess,
fetchReleasesGraphQl,
fetchReleasesRest,
receiveReleasesError,
} from '~/releases/stores/modules/list/actions';
import createState from '~/releases/stores/modules/list/state';
import * as types from '~/releases/stores/modules/list/mutation_types';
import api from '~/api';
import { gqClient, convertGraphQLResponse } from '~/releases/util';
import { parseIntPagination, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import {
normalizeHeaders,
parseIntPagination,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
import {
pageInfoHeadersWithoutPagination,
releases as originalReleases,
graphqlReleasesResponse as originalGraphqlReleasesResponse,
} from '../../../mock_data';
import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
import { PAGE_SIZE } from '~/releases/constants';
describe('Releases State actions', () => {
let mockedState;
let pageInfo;
let releases;
let graphqlReleasesResponse;
const projectPath = 'root/test-project';
const projectId = 19;
const before = 'testBeforeCursor';
const after = 'testAfterCursor';
const page = 2;
beforeEach(() => {
mockedState = {
......@@ -33,178 +40,261 @@ describe('Releases State actions', () => {
projectId,
projectPath,
}),
featureFlags: {
graphqlReleaseData: true,
graphqlReleasesPage: true,
graphqlMilestoneStats: true,
},
};
pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination);
releases = convertObjectPropsToCamelCase(originalReleases, { deep: true });
graphqlReleasesResponse = cloneDeep(originalGraphqlReleasesResponse);
});
describe('requestReleases', () => {
it('should commit REQUEST_RELEASES mutation', done => {
testAction(requestReleases, null, mockedState, [{ type: types.REQUEST_RELEASES }], [], done);
describe('when all the necessary GraphQL feature flags are enabled', () => {
beforeEach(() => {
mockedState.useGraphQLEndpoint = true;
});
describe('fetchReleases', () => {
it('dispatches fetchReleasesGraphQl with before and after parameters', () => {
return testAction(
fetchReleases,
{ before, after, page },
mockedState,
[],
[
{
type: 'fetchReleasesGraphQl',
payload: { before, after },
},
],
);
});
});
});
describe('fetchReleases', () => {
describe('success', () => {
it('dispatches requestReleases and receiveReleasesSuccess', done => {
jest.spyOn(gqClient, 'query').mockImplementation(({ query, variables }) => {
expect(query).toBe(allReleasesQuery);
expect(variables).toEqual({
fullPath: projectPath,
describe('when at least one of the GraphQL feature flags is disabled', () => {
beforeEach(() => {
mockedState.useGraphQLEndpoint = false;
});
describe('fetchReleases', () => {
it('dispatches fetchReleasesRest with a page parameter', () => {
return testAction(
fetchReleases,
{ before, after, page },
mockedState,
[],
[
{
type: 'fetchReleasesRest',
payload: { page },
},
],
);
});
});
});
describe('fetchReleasesGraphQl', () => {
describe('GraphQL query variables', () => {
let vuexParams;
beforeEach(() => {
jest.spyOn(gqClient, 'query');
vuexParams = { dispatch: jest.fn(), commit: jest.fn(), state: mockedState };
});
describe('when neither a before nor an after parameter is provided', () => {
beforeEach(() => {
fetchReleasesGraphQl(vuexParams, { before: undefined, after: undefined });
});
it('makes a GraphQl query with a first variable', () => {
expect(gqClient.query).toHaveBeenCalledWith({
query: allReleasesQuery,
variables: { fullPath: projectPath, first: PAGE_SIZE },
});
return Promise.resolve(graphqlReleasesResponse);
});
});
testAction(
fetchReleases,
describe('when only a before parameter is provided', () => {
beforeEach(() => {
fetchReleasesGraphQl(vuexParams, { before, after: undefined });
});
it('makes a GraphQl query with last and before variables', () => {
expect(gqClient.query).toHaveBeenCalledWith({
query: allReleasesQuery,
variables: { fullPath: projectPath, last: PAGE_SIZE, before },
});
});
});
describe('when only an after parameter is provided', () => {
beforeEach(() => {
fetchReleasesGraphQl(vuexParams, { before: undefined, after });
});
it('makes a GraphQl query with first and after variables', () => {
expect(gqClient.query).toHaveBeenCalledWith({
query: allReleasesQuery,
variables: { fullPath: projectPath, first: PAGE_SIZE, after },
});
});
});
describe('when both before and after parameters are provided', () => {
it('throws an error', () => {
const callFetchReleasesGraphQl = () => {
fetchReleasesGraphQl(vuexParams, { before, after });
};
expect(callFetchReleasesGraphQl).toThrowError(
'Both a `before` and an `after` parameter were provided to fetchReleasesGraphQl. These parameters cannot be used together.',
);
});
});
});
describe('when the request is successful', () => {
beforeEach(() => {
jest.spyOn(gqClient, 'query').mockResolvedValue(graphqlReleasesResponse);
});
it(`commits ${types.REQUEST_RELEASES} and ${types.RECEIVE_RELEASES_SUCCESS}`, () => {
const convertedResponse = convertGraphQLResponse(graphqlReleasesResponse);
return testAction(
fetchReleasesGraphQl,
{},
mockedState,
[],
[
{
type: 'requestReleases',
type: types.REQUEST_RELEASES,
},
{
payload: convertGraphQLResponse(graphqlReleasesResponse),
type: 'receiveReleasesSuccess',
type: types.RECEIVE_RELEASES_SUCCESS,
payload: {
data: convertedResponse.data,
graphQlPageInfo: convertedResponse.paginationInfo,
},
},
],
done,
[],
);
});
});
describe('error', () => {
it('dispatches requestReleases and receiveReleasesError', done => {
jest.spyOn(gqClient, 'query').mockRejectedValue();
describe('when the request fails', () => {
beforeEach(() => {
jest.spyOn(gqClient, 'query').mockRejectedValue(new Error('Something went wrong!'));
});
testAction(
fetchReleases,
it(`commits ${types.REQUEST_RELEASES} and dispatch receiveReleasesError`, () => {
return testAction(
fetchReleasesGraphQl,
{},
mockedState,
[],
[
{
type: 'requestReleases',
type: types.REQUEST_RELEASES,
},
],
[
{
type: 'receiveReleasesError',
},
],
done,
);
});
});
});
describe('fetchReleasesRest', () => {
describe('REST query parameters', () => {
let vuexParams;
describe('when the graphqlReleaseData feature flag is disabled', () => {
beforeEach(() => {
mockedState.featureFlags.graphqlReleasesPage = false;
});
jest
.spyOn(api, 'releases')
.mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination });
describe('success', () => {
it('dispatches requestReleases and receiveReleasesSuccess', done => {
jest.spyOn(api, 'releases').mockImplementation((id, options) => {
expect(id).toBe(projectId);
expect(options.page).toBe('1');
return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination });
});
vuexParams = { dispatch: jest.fn(), commit: jest.fn(), state: mockedState };
});
testAction(
fetchReleases,
{},
mockedState,
[],
[
{
type: 'requestReleases',
},
{
payload: { data: releases, headers: pageInfoHeadersWithoutPagination },
type: 'receiveReleasesSuccess',
},
],
done,
);
describe('when a page parameter is provided', () => {
beforeEach(() => {
fetchReleasesRest(vuexParams, { page: 2 });
});
it('dispatches requestReleases and receiveReleasesSuccess on page two', done => {
jest.spyOn(api, 'releases').mockImplementation((_, options) => {
expect(options.page).toBe('2');
return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination });
});
testAction(
fetchReleases,
{ page: '2' },
mockedState,
[],
[
{
type: 'requestReleases',
},
{
payload: { data: releases, headers: pageInfoHeadersWithoutPagination },
type: 'receiveReleasesSuccess',
},
],
done,
);
it('makes a REST query with a page query parameter', () => {
expect(api.releases).toHaveBeenCalledWith(projectId, { page });
});
});
});
describe('error', () => {
it('dispatches requestReleases and receiveReleasesError', done => {
jest.spyOn(api, 'releases').mockReturnValue(Promise.reject());
describe('when the request is successful', () => {
beforeEach(() => {
jest
.spyOn(api, 'releases')
.mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination });
});
testAction(
fetchReleases,
{},
mockedState,
[],
[
{
type: 'requestReleases',
},
{
type: 'receiveReleasesError',
it(`commits ${types.REQUEST_RELEASES} and ${types.RECEIVE_RELEASES_SUCCESS}`, () => {
return testAction(
fetchReleasesRest,
{},
mockedState,
[
{
type: types.REQUEST_RELEASES,
},
{
type: types.RECEIVE_RELEASES_SUCCESS,
payload: {
data: convertObjectPropsToCamelCase(releases, { deep: true }),
restPageInfo: parseIntPagination(
normalizeHeaders(pageInfoHeadersWithoutPagination),
),
},
],
done,
);
});
},
],
[],
);
});
});
});
describe('receiveReleasesSuccess', () => {
it('should commit RECEIVE_RELEASES_SUCCESS mutation', done => {
testAction(
receiveReleasesSuccess,
{ data: releases, headers: pageInfoHeadersWithoutPagination },
mockedState,
[{ type: types.RECEIVE_RELEASES_SUCCESS, payload: { pageInfo, data: releases } }],
[],
done,
);
describe('when the request fails', () => {
beforeEach(() => {
jest.spyOn(api, 'releases').mockRejectedValue(new Error('Something went wrong!'));
});
it(`commits ${types.REQUEST_RELEASES} and dispatch receiveReleasesError`, () => {
return testAction(
fetchReleasesRest,
{},
mockedState,
[
{
type: types.REQUEST_RELEASES,
},
],
[
{
type: 'receiveReleasesError',
},
],
);
});
});
});
describe('receiveReleasesError', () => {
it('should commit RECEIVE_RELEASES_ERROR mutation', done => {
testAction(
it('should commit RECEIVE_RELEASES_ERROR mutation', () => {
return testAction(
receiveReleasesError,
null,
mockedState,
[{ type: types.RECEIVE_RELEASES_ERROR }],
[],
done,
);
});
});
......
......@@ -2,15 +2,22 @@ import createState from '~/releases/stores/modules/list/state';
import mutations from '~/releases/stores/modules/list/mutations';
import * as types from '~/releases/stores/modules/list/mutation_types';
import { parseIntPagination } from '~/lib/utils/common_utils';
import { pageInfoHeadersWithoutPagination, releases } from '../../../mock_data';
import {
pageInfoHeadersWithoutPagination,
releases,
graphqlReleasesResponse,
} from '../../../mock_data';
import { convertGraphQLResponse } from '~/releases/util';
describe('Releases Store Mutations', () => {
let stateCopy;
let pageInfo;
let restPageInfo;
let graphQlPageInfo;
beforeEach(() => {
stateCopy = createState({});
pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination);
restPageInfo = parseIntPagination(pageInfoHeadersWithoutPagination);
graphQlPageInfo = convertGraphQLResponse(graphqlReleasesResponse).paginationInfo;
});
describe('REQUEST_RELEASES', () => {
......@@ -23,7 +30,11 @@ describe('Releases Store Mutations', () => {
describe('RECEIVE_RELEASES_SUCCESS', () => {
beforeEach(() => {
mutations[types.RECEIVE_RELEASES_SUCCESS](stateCopy, { pageInfo, data: releases });
mutations[types.RECEIVE_RELEASES_SUCCESS](stateCopy, {
restPageInfo,
graphQlPageInfo,
data: releases,
});
});
it('sets is loading to false', () => {
......@@ -38,18 +49,29 @@ describe('Releases Store Mutations', () => {
expect(stateCopy.releases).toEqual(releases);
});
it('sets pageInfo', () => {
expect(stateCopy.pageInfo).toEqual(pageInfo);
it('sets restPageInfo', () => {
expect(stateCopy.restPageInfo).toEqual(restPageInfo);
});
it('sets graphQlPageInfo', () => {
expect(stateCopy.graphQlPageInfo).toEqual(graphQlPageInfo);
});
});
describe('RECEIVE_RELEASES_ERROR', () => {
it('resets data', () => {
mutations[types.RECEIVE_RELEASES_SUCCESS](stateCopy, {
restPageInfo,
graphQlPageInfo,
data: releases,
});
mutations[types.RECEIVE_RELEASES_ERROR](stateCopy);
expect(stateCopy.isLoading).toEqual(false);
expect(stateCopy.releases).toEqual([]);
expect(stateCopy.pageInfo).toEqual({});
expect(stateCopy.restPageInfo).toEqual({});
expect(stateCopy.graphQlPageInfo).toEqual({});
});
});
});
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