Commit 1b62b7f3 authored by Shinya Maeda's avatar Shinya Maeda Committed by Andrew Fontaine

Remove 3 GraphQL/release-related feature flags [RUN ALL RSPEC] [RUN AS-IF-FOSS]

parent 4296b032
...@@ -51,12 +51,8 @@ export default { ...@@ -51,12 +51,8 @@ export default {
}), }),
fetchReleases() { fetchReleases() {
this.fetchReleasesStoreAction({ this.fetchReleasesStoreAction({
// these two parameters are only used in "GraphQL mode"
before: getParameterByName('before'), before: getParameterByName('before'),
after: getParameterByName('after'), after: getParameterByName('after'),
// this parameter is only used when in "REST mode"
page: getParameterByName('page'),
}); });
}, },
}, },
...@@ -73,17 +69,17 @@ export default { ...@@ -73,17 +69,17 @@ export default {
:aria-describedby="shouldRenderEmptyState && 'releases-description'" :aria-describedby="shouldRenderEmptyState && 'releases-description'"
category="primary" category="primary"
variant="success" variant="success"
class="js-new-release-btn" data-testid="new-release-button"
> >
{{ __('New release') }} {{ __('New release') }}
</gl-button> </gl-button>
</div> </div>
<release-skeleton-loader v-if="isLoading" class="js-loading" /> <release-skeleton-loader v-if="isLoading" />
<gl-empty-state <gl-empty-state
v-else-if="shouldRenderEmptyState" v-else-if="shouldRenderEmptyState"
class="js-empty-state" data-testid="empty-state"
:title="__('Getting started with releases')" :title="__('Getting started with releases')"
:svg-path="illustrationPath" :svg-path="illustrationPath"
> >
...@@ -101,7 +97,7 @@ export default { ...@@ -101,7 +97,7 @@ export default {
</template> </template>
</gl-empty-state> </gl-empty-state>
<div v-else-if="shouldRenderSuccessState" class="js-success-state"> <div v-else-if="shouldRenderSuccessState" data-testid="success-state">
<release-block <release-block
v-for="(release, index) in releases" v-for="(release, index) in releases"
:key="index" :key="index"
......
<script> <script>
import { mapGetters } from 'vuex';
import ReleasesPaginationGraphql from './releases_pagination_graphql.vue'; import ReleasesPaginationGraphql from './releases_pagination_graphql.vue';
import ReleasesPaginationRest from './releases_pagination_rest.vue'; import ReleasesPaginationRest from './releases_pagination_rest.vue';
...@@ -7,7 +6,12 @@ export default { ...@@ -7,7 +6,12 @@ export default {
name: 'ReleasesPagination', name: 'ReleasesPagination',
components: { ReleasesPaginationGraphql, ReleasesPaginationRest }, components: { ReleasesPaginationGraphql, ReleasesPaginationRest },
computed: { computed: {
...mapGetters(['useGraphQLEndpoint']), // TODO: Remove the ReleasesPaginationRest component since
// it is never shown.
// https://gitlab.com/gitlab-org/gitlab/-/issues/329267
useGraphQLEndpoint() {
return true;
},
}, },
}; };
</script> </script>
......
...@@ -15,11 +15,6 @@ export default () => { ...@@ -15,11 +15,6 @@ export default () => {
modules: { modules: {
index: createIndexModule(el.dataset), index: createIndexModule(el.dataset),
}, },
featureFlags: {
graphqlReleaseData: Boolean(gon.features?.graphqlReleaseData),
graphqlReleasesPage: Boolean(gon.features?.graphqlReleasesPage),
graphqlMilestoneStats: Boolean(gon.features?.graphqlMilestoneStats),
},
}), }),
render: (h) => h(ReleaseIndexApp), render: (h) => h(ReleaseIndexApp),
}); });
......
/**
* @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 Vuex from 'vuex';
import * as getters from './getters';
export default ({ modules, featureFlags }) => export default ({ modules, featureFlags }) =>
new Vuex.Store({ new Vuex.Store({
modules, modules,
state: { featureFlags }, state: { featureFlags },
getters,
}); });
import api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
normalizeHeaders,
parseIntPagination,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { PAGE_SIZE } from '~/releases/constants';
import allReleasesQuery from '~/releases/queries/all_releases.query.graphql'; import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
import { PAGE_SIZE } from '../../../constants'; import { gqClient, convertAllReleasesGraphQLResponse } from '~/releases/util';
import { gqClient, convertAllReleasesGraphQLResponse } from '../../../util';
import * as types from './mutation_types'; import * as types from './mutation_types';
/** /**
* Gets a paginated list of releases from the server * Gets a paginated list of releases from the GraphQL endpoint
* *
* @param {Object} vuexParams * @param {Object} vuexParams
* @param {Object} actionParams * @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, * @param {String} [actionParams.before] A GraphQL cursor. If provided,
* the items returned will proceed the provided cursor (this parameter is only * the items returned will proceed the provided cursor.
* used when fetching results from the GraphQL API).
* @param {String} [actionParams.after] A GraphQL cursor. If provided, * @param {String} [actionParams.after] A GraphQL cursor. If provided,
* the items returned will follow the provided cursor (this parameter is only * the items returned will follow the provided cursor.
* used when fetching results from the GraphQL API).
*/ */
export const fetchReleases = ({ dispatch, rootGetters }, { page = 1, before, after }) => { export const fetchReleases = ({ dispatch, commit, state }, { before, after }) => {
if (rootGetters.useGraphQLEndpoint) {
dispatch('fetchReleasesGraphQl', { before, after });
} else {
dispatch('fetchReleasesRest', { page });
}
};
/**
* Gets a paginated list of releases from the GraphQL endpoint
*/
export const fetchReleasesGraphQl = (
{ dispatch, commit, state },
{ before = null, after = null },
) => {
commit(types.REQUEST_RELEASES); commit(types.REQUEST_RELEASES);
const { sort, orderBy } = state.sorting; const { sort, orderBy } = state.sorting;
...@@ -55,7 +31,7 @@ export const fetchReleasesGraphQl = ( ...@@ -55,7 +31,7 @@ export const fetchReleasesGraphQl = (
paginationParams = { first: PAGE_SIZE, after }; paginationParams = { first: PAGE_SIZE, after };
} else { } else {
throw new Error( throw new Error(
'Both a `before` and an `after` parameter were provided to fetchReleasesGraphQl. These parameters cannot be used together.', 'Both a `before` and an `after` parameter were provided to fetchReleases. These parameters cannot be used together.',
); );
} }
...@@ -79,28 +55,6 @@ export const fetchReleasesGraphQl = ( ...@@ -79,28 +55,6 @@ export const fetchReleasesGraphQl = (
.catch(() => dispatch('receiveReleasesError')); .catch(() => dispatch('receiveReleasesError'));
}; };
/**
* Gets a paginated list of releases from the REST endpoint
*/
export const fetchReleasesRest = ({ dispatch, commit, state }, { page }) => {
commit(types.REQUEST_RELEASES);
const { sort, orderBy } = state.sorting;
api
.releases(state.projectId, { page, sort, order_by: orderBy })
.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 }) => { export const receiveReleasesError = ({ commit }) => {
commit(types.RECEIVE_RELEASES_ERROR); commit(types.RECEIVE_RELEASES_ERROR);
createFlash(__('An error occurred while fetching the releases. Please try again.')); createFlash(__('An error occurred while fetching the releases. Please try again.'));
......
...@@ -8,11 +8,6 @@ class Projects::ReleasesController < Projects::ApplicationController ...@@ -8,11 +8,6 @@ class Projects::ReleasesController < Projects::ApplicationController
# We have to check `download_code` permission because detail URL path # We have to check `download_code` permission because detail URL path
# contains git-tag name. # contains git-tag name.
before_action :authorize_download_code!, except: [:index] before_action :authorize_download_code!, except: [:index]
before_action do
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: true)
end
before_action :authorize_update_release!, only: %i[edit update] before_action :authorize_update_release!, only: %i[edit update]
before_action :authorize_create_release!, only: :new before_action :authorize_create_release!, only: :new
......
...@@ -15,8 +15,6 @@ module Resolvers ...@@ -15,8 +15,6 @@ module Resolvers
end end
def resolve(tag_name:) def resolve(tag_name:)
return unless Feature.enabled?(:graphql_release_data, project, default_enabled: true)
ReleasesFinder.new( ReleasesFinder.new(
project, project,
current_user, current_user,
......
...@@ -23,8 +23,6 @@ module Resolvers ...@@ -23,8 +23,6 @@ module Resolvers
}.freeze }.freeze
def resolve(sort:) def resolve(sort:)
return unless Feature.enabled?(:graphql_release_data, project, default_enabled: true)
ReleasesFinder.new( ReleasesFinder.new(
project, project,
current_user, current_user,
......
...@@ -57,8 +57,6 @@ module Types ...@@ -57,8 +57,6 @@ module Types
description: 'Milestone statistics.' description: 'Milestone statistics.'
def stats def stats
return unless Feature.enabled?(:graphql_milestone_stats, milestone.project || milestone.group, default_enabled: true)
milestone milestone
end end
end end
......
---
title: Remove graphql_release_data, graphql_milestone_stats, and graphql_releases_page
feature flags
merge_request: 60390
author:
type: other
---
name: graphql_milestone_stats
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35066
rollout_issue_url:
milestone: '13.2'
type: development
group: group::release
default_enabled: true
---
name: graphql_release_data
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30753
rollout_issue_url:
milestone: '13.0'
type: development
group: group::release
default_enabled: true
---
name: graphql_releases_page
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33095
rollout_issue_url:
milestone: '13.4'
type: development
group: group::release
default_enabled: true
...@@ -147,15 +147,5 @@ RSpec.describe 'User views releases', :js do ...@@ -147,15 +147,5 @@ RSpec.describe 'User views releases', :js do
end end
end end
context 'when the graphql_releases_page feature flag is enabled' do it_behaves_like 'releases page'
it_behaves_like 'releases page'
end
context 'when the graphql_releases_page feature flag is disabled' do
before do
stub_feature_flags(graphql_releases_page: false)
end
it_behaves_like 'releases page'
end
end end
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { range as rge } from 'lodash'; import { merge } from 'lodash';
import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures'; import { getParameterByName } from '~/lib/utils/common_utils';
import waitForPromises from 'helpers/wait_for_promises'; import AppIndex from '~/releases/components/app_index.vue';
import api from '~/api'; import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleasesApp from '~/releases/components/app_index.vue';
import ReleasesPagination from '~/releases/components/releases_pagination.vue'; import ReleasesPagination from '~/releases/components/releases_pagination.vue';
import createStore from '~/releases/stores'; import ReleasesSort from '~/releases/components/releases_sort.vue';
import createIndexModule from '~/releases/stores/modules/index';
import { pageInfoHeadersWithoutPagination, pageInfoHeadersWithPagination } from '../mock_data';
jest.mock('~/lib/utils/common_utils', () => ({ jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'), ...jest.requireActual('~/lib/utils/common_utils'),
getParameterByName: jest.fn().mockImplementation((paramName) => { getParameterByName: jest.fn(),
return `${paramName}_param_value`;
}),
})); }));
const localVue = createLocalVue(); Vue.use(Vuex);
localVue.use(Vuex);
const release = getJSONFixture('api/releases/release.json'); describe('app_index.vue', () => {
const releases = [release];
describe('Releases App ', () => {
let wrapper; let wrapper;
let fetchReleaseSpy; let fetchReleasesSpy;
let urlParams;
const paginatedReleases = rge(21).map((index) => ({
...convertObjectPropsToCamelCase(release, { deep: true }), const createComponent = (storeUpdates) => {
tagName: `${index}.00`, wrapper = shallowMount(AppIndex, {
})); store: new Vuex.Store({
modules: {
const defaultInitialState = { index: merge(
projectId: 'gitlab-ce', {
projectPath: 'gitlab-org/gitlab-ce', namespaced: true,
documentationPath: 'help/releases', actions: {
illustrationPath: 'illustration/path', fetchReleases: fetchReleasesSpy,
},
state: {
isLoading: true,
releases: [],
},
},
storeUpdates,
),
},
}),
});
}; };
const createComponent = (stateUpdates = {}) => { beforeEach(() => {
const indexModule = createIndexModule({ fetchReleasesSpy = jest.fn();
...defaultInitialState, getParameterByName.mockImplementation((paramName) => urlParams[paramName]);
...stateUpdates, });
afterEach(() => {
wrapper.destroy();
});
// Finders
const findLoadingIndicator = () => wrapper.find(ReleaseSkeletonLoader);
const findEmptyState = () => wrapper.find('[data-testid="empty-state"]');
const findSuccessState = () => wrapper.find('[data-testid="success-state"]');
const findPagination = () => wrapper.find(ReleasesPagination);
const findSortControls = () => wrapper.find(ReleasesSort);
const findNewReleaseButton = () => wrapper.find('[data-testid="new-release-button"]');
// Expectations
const expectLoadingIndicator = (shouldExist) => {
it(`${shouldExist ? 'renders' : 'does not render'} a loading indicator`, () => {
expect(findLoadingIndicator().exists()).toBe(shouldExist);
}); });
};
fetchReleaseSpy = jest.spyOn(indexModule.actions, 'fetchReleases'); const expectEmptyState = (shouldExist) => {
it(`${shouldExist ? 'renders' : 'does not render'} an empty state`, () => {
expect(findEmptyState().exists()).toBe(shouldExist);
});
};
const store = createStore({ const expectSuccessState = (shouldExist) => {
modules: { index: indexModule }, it(`${shouldExist ? 'renders' : 'does not render'} the success state`, () => {
featureFlags: { expect(findSuccessState().exists()).toBe(shouldExist);
graphqlReleaseData: true,
graphqlReleasesPage: false,
graphqlMilestoneStats: true,
},
}); });
};
wrapper = shallowMount(ReleasesApp, { const expectPagination = (shouldExist) => {
store, it(`${shouldExist ? 'renders' : 'does not render'} the pagination controls`, () => {
localVue, expect(findPagination().exists()).toBe(shouldExist);
}); });
}; };
afterEach(() => { const expectNewReleaseButton = (shouldExist) => {
wrapper.destroy(); it(`${shouldExist ? 'renders' : 'does not render'} the "New release" button`, () => {
}); expect(findNewReleaseButton().exists()).toBe(shouldExist);
});
};
// Tests
describe('on startup', () => { describe('on startup', () => {
beforeEach(() => { it.each`
jest before | after
.spyOn(api, 'releases') ${null} | ${null}
.mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination }); ${'before_param_value'} | ${null}
${null} | ${'after_param_value'}
`(
'calls fetchRelease with the correct parameters based on the curent query parameters: before: $before, after: $after',
({ before, after }) => {
urlParams = { before, after };
createComponent();
expect(fetchReleasesSpy).toHaveBeenCalledTimes(1);
expect(fetchReleasesSpy).toHaveBeenCalledWith(expect.anything(), urlParams);
},
);
});
describe('when the request to fetch releases has not yet completed', () => {
beforeEach(() => {
createComponent(); createComponent();
}); });
it('calls fetchRelease with the page, before, and after parameters', () => { expectLoadingIndicator(true);
expect(fetchReleaseSpy).toHaveBeenCalledTimes(1); expectEmptyState(false);
expect(fetchReleaseSpy).toHaveBeenCalledWith(expect.anything(), { expectSuccessState(false);
page: 'page_param_value', expectPagination(false);
before: 'before_param_value',
after: 'after_param_value',
});
});
}); });
describe('while loading', () => { describe('when the request fails', () => {
beforeEach(() => { beforeEach(() => {
jest createComponent({
.spyOn(api, 'releases') state: {
// Need to defer the return value here to the next stack, isLoading: false,
// otherwise the loading state disappears before our test even starts. hasError: true,
.mockImplementation(() => waitForPromises().then(() => ({ data: [], headers: {} }))); },
});
createComponent();
}); });
it('renders loading icon', () => { expectLoadingIndicator(false);
expect(wrapper.find('.js-loading').exists()).toBe(true); expectEmptyState(false);
expect(wrapper.find('.js-empty-state').exists()).toBe(false); expectSuccessState(false);
expect(wrapper.find('.js-success-state').exists()).toBe(false); expectPagination(true);
expect(wrapper.find(ReleasesPagination).exists()).toBe(false);
});
}); });
describe('with successful request', () => { describe('when the request succeeds but returns no releases', () => {
beforeEach(() => { beforeEach(() => {
jest createComponent({
.spyOn(api, 'releases') state: {
.mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination }); isLoading: false,
},
createComponent(); });
}); });
it('renders success state', () => { expectLoadingIndicator(false);
expect(wrapper.find('.js-loading').exists()).toBe(false); expectEmptyState(true);
expect(wrapper.find('.js-empty-state').exists()).toBe(false); expectSuccessState(false);
expect(wrapper.find('.js-success-state').exists()).toBe(true); expectPagination(true);
expect(wrapper.find(ReleasesPagination).exists()).toBe(true);
});
}); });
describe('with successful request and pagination', () => { describe('when the request succeeds and includes at least one release', () => {
beforeEach(() => { beforeEach(() => {
jest createComponent({
.spyOn(api, 'releases') state: {
.mockResolvedValue({ data: paginatedReleases, headers: pageInfoHeadersWithPagination }); isLoading: false,
releases: [{}],
createComponent(); },
});
}); });
it('renders success state', () => { expectLoadingIndicator(false);
expect(wrapper.find('.js-loading').exists()).toBe(false); expectEmptyState(false);
expect(wrapper.find('.js-empty-state').exists()).toBe(false); expectSuccessState(true);
expect(wrapper.find('.js-success-state').exists()).toBe(true); expectPagination(true);
expect(wrapper.find(ReleasesPagination).exists()).toBe(true);
});
}); });
describe('with empty request', () => { describe('sorting', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(api, 'releases').mockResolvedValue({ data: [], headers: {} });
createComponent(); createComponent();
}); });
it('renders empty state', () => { it('renders the sort controls', () => {
expect(wrapper.find('.js-loading').exists()).toBe(false); expect(findSortControls().exists()).toBe(true);
expect(wrapper.find('.js-empty-state').exists()).toBe(true);
expect(wrapper.find('.js-success-state').exists()).toBe(false);
}); });
});
describe('"New release" button', () => { it('calls the fetchReleases store method when the sort is updated', () => {
const findNewReleaseButton = () => wrapper.find('.js-new-release-btn'); fetchReleasesSpy.mockClear();
beforeEach(() => { findSortControls().vm.$emit('sort:changed');
jest.spyOn(api, 'releases').mockResolvedValue({ data: [], headers: {} });
expect(fetchReleasesSpy).toHaveBeenCalledTimes(1);
}); });
});
describe('when the user is allowed to create a new Release', () => { describe('"New release" button', () => {
const newReleasePath = 'path/to/new/release'; describe('when the user is allowed to create releases', () => {
const newReleasePath = 'path/to/new/release/page';
beforeEach(() => { beforeEach(() => {
createComponent({ newReleasePath }); createComponent({ state: { newReleasePath } });
}); });
it('renders the "New release" button', () => { expectNewReleaseButton(true);
expect(findNewReleaseButton().exists()).toBe(true);
});
it('renders the "New release" button with the correct href', () => { it('renders the button with the correct href', () => {
expect(findNewReleaseButton().attributes('href')).toBe(newReleasePath); expect(findNewReleaseButton().attributes('href')).toBe(newReleasePath);
}); });
}); });
describe('when the user is not allowed to create a new Release', () => { describe('when the user is not allowed to create releases', () => {
beforeEach(() => createComponent()); beforeEach(() => {
createComponent();
it('does not render the "New release" button', () => {
expect(findNewReleaseButton().exists()).toBe(false);
}); });
expectNewReleaseButton(false);
}); });
}); });
describe('when the back button is pressed', () => { describe("when the browser's back button is pressed", () => {
beforeEach(() => { beforeEach(() => {
jest urlParams = {
.spyOn(api, 'releases') before: 'before_param_value',
.mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination }); };
createComponent(); createComponent();
fetchReleaseSpy.mockClear(); fetchReleasesSpy.mockClear();
window.dispatchEvent(new PopStateEvent('popstate')); window.dispatchEvent(new PopStateEvent('popstate'));
}); });
it('calls fetchRelease with the page parameter', () => { it('calls the fetchRelease store method with the parameters from the URL query', () => {
expect(fetchReleaseSpy).toHaveBeenCalledTimes(1); expect(fetchReleasesSpy).toHaveBeenCalledTimes(1);
expect(fetchReleaseSpy).toHaveBeenCalledWith(expect.anything(), { expect(fetchReleasesSpy).toHaveBeenCalledWith(expect.anything(), urlParams);
page: 'page_param_value',
before: 'before_param_value',
after: 'after_param_value',
});
}); });
}); });
}); });
...@@ -20,6 +20,10 @@ describe('~/releases/components/releases_pagination.vue', () => { ...@@ -20,6 +20,10 @@ describe('~/releases/components/releases_pagination.vue', () => {
wrapper = shallowMount(ReleasesPagination, { store, localVue }); wrapper = shallowMount(ReleasesPagination, { store, localVue });
}; };
beforeEach(() => {
createComponent(true);
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
...@@ -28,25 +32,8 @@ describe('~/releases/components/releases_pagination.vue', () => { ...@@ -28,25 +32,8 @@ describe('~/releases/components/releases_pagination.vue', () => {
const findRestPagination = () => wrapper.find(ReleasesPaginationRest); const findRestPagination = () => wrapper.find(ReleasesPaginationRest);
const findGraphQlPagination = () => wrapper.find(ReleasesPaginationGraphql); const findGraphQlPagination = () => wrapper.find(ReleasesPaginationGraphql);
describe('when one of necessary feature flags is disabled', () => { it('renders the GraphQL pagination component', () => {
beforeEach(() => { expect(findGraphQlPagination().exists()).toBe(true);
createComponent(false); expect(findRestPagination().exists()).toBe(false);
});
it('renders the REST pagination component', () => {
expect(findRestPagination().exists()).toBe(true);
expect(findGraphQlPagination().exists()).toBe(false);
});
});
describe('when all the necessary feature flags are enabled', () => {
beforeEach(() => {
createComponent(true);
});
it('renders the GraphQL pagination component', () => {
expect(findGraphQlPagination().exists()).toBe(true);
expect(findRestPagination().exists()).toBe(false);
});
}); });
}); });
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 { cloneDeep } from 'lodash';
import { getJSONFixture } from 'helpers/fixtures'; import { getJSONFixture } from 'helpers/fixtures';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import api from '~/api';
import {
normalizeHeaders,
parseIntPagination,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
import { PAGE_SIZE } from '~/releases/constants'; import { PAGE_SIZE } from '~/releases/constants';
import allReleasesQuery from '~/releases/queries/all_releases.query.graphql'; import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
import { import {
fetchReleases, fetchReleases,
fetchReleasesGraphQl,
fetchReleasesRest,
receiveReleasesError, receiveReleasesError,
setSorting, setSorting,
} from '~/releases/stores/modules/index/actions'; } from '~/releases/stores/modules/index/actions';
import * as types from '~/releases/stores/modules/index/mutation_types'; import * as types from '~/releases/stores/modules/index/mutation_types';
import createState from '~/releases/stores/modules/index/state'; import createState from '~/releases/stores/modules/index/state';
import { gqClient, convertAllReleasesGraphQLResponse } from '~/releases/util'; import { gqClient, convertAllReleasesGraphQLResponse } from '~/releases/util';
import { pageInfoHeadersWithoutPagination } from '../../../mock_data';
const originalRelease = getJSONFixture('api/releases/release.json');
const originalReleases = [originalRelease];
const originalGraphqlReleasesResponse = getJSONFixture( const originalGraphqlReleasesResponse = getJSONFixture(
'graphql/releases/queries/all_releases.query.graphql.json', 'graphql/releases/queries/all_releases.query.graphql.json',
...@@ -30,14 +18,12 @@ const originalGraphqlReleasesResponse = getJSONFixture( ...@@ -30,14 +18,12 @@ const originalGraphqlReleasesResponse = getJSONFixture(
describe('Releases State actions', () => { describe('Releases State actions', () => {
let mockedState; let mockedState;
let releases;
let graphqlReleasesResponse; let graphqlReleasesResponse;
const projectPath = 'root/test-project'; const projectPath = 'root/test-project';
const projectId = 19; const projectId = 19;
const before = 'testBeforeCursor'; const before = 'testBeforeCursor';
const after = 'testAfterCursor'; const after = 'testAfterCursor';
const page = 2;
beforeEach(() => { beforeEach(() => {
mockedState = { mockedState = {
...@@ -47,57 +33,10 @@ describe('Releases State actions', () => { ...@@ -47,57 +33,10 @@ describe('Releases State actions', () => {
}), }),
}; };
releases = convertObjectPropsToCamelCase(originalReleases, { deep: true });
graphqlReleasesResponse = cloneDeep(originalGraphqlReleasesResponse); graphqlReleasesResponse = cloneDeep(originalGraphqlReleasesResponse);
}); });
describe('when all the necessary GraphQL feature flags are enabled', () => { describe('fetchReleases', () => {
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('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', () => { describe('GraphQL query variables', () => {
let vuexParams; let vuexParams;
...@@ -109,7 +48,7 @@ describe('Releases State actions', () => { ...@@ -109,7 +48,7 @@ describe('Releases State actions', () => {
describe('when neither a before nor an after parameter is provided', () => { describe('when neither a before nor an after parameter is provided', () => {
beforeEach(() => { beforeEach(() => {
fetchReleasesGraphQl(vuexParams, { before: undefined, after: undefined }); fetchReleases(vuexParams, { before: undefined, after: undefined });
}); });
it('makes a GraphQl query with a first variable', () => { it('makes a GraphQl query with a first variable', () => {
...@@ -122,7 +61,7 @@ describe('Releases State actions', () => { ...@@ -122,7 +61,7 @@ describe('Releases State actions', () => {
describe('when only a before parameter is provided', () => { describe('when only a before parameter is provided', () => {
beforeEach(() => { beforeEach(() => {
fetchReleasesGraphQl(vuexParams, { before, after: undefined }); fetchReleases(vuexParams, { before, after: undefined });
}); });
it('makes a GraphQl query with last and before variables', () => { it('makes a GraphQl query with last and before variables', () => {
...@@ -135,7 +74,7 @@ describe('Releases State actions', () => { ...@@ -135,7 +74,7 @@ describe('Releases State actions', () => {
describe('when only an after parameter is provided', () => { describe('when only an after parameter is provided', () => {
beforeEach(() => { beforeEach(() => {
fetchReleasesGraphQl(vuexParams, { before: undefined, after }); fetchReleases(vuexParams, { before: undefined, after });
}); });
it('makes a GraphQl query with first and after variables', () => { it('makes a GraphQl query with first and after variables', () => {
...@@ -148,12 +87,12 @@ describe('Releases State actions', () => { ...@@ -148,12 +87,12 @@ describe('Releases State actions', () => {
describe('when both before and after parameters are provided', () => { describe('when both before and after parameters are provided', () => {
it('throws an error', () => { it('throws an error', () => {
const callFetchReleasesGraphQl = () => { const callFetchReleases = () => {
fetchReleasesGraphQl(vuexParams, { before, after }); fetchReleases(vuexParams, { before, after });
}; };
expect(callFetchReleasesGraphQl).toThrowError( expect(callFetchReleases).toThrowError(
'Both a `before` and an `after` parameter were provided to fetchReleasesGraphQl. These parameters cannot be used together.', 'Both a `before` and an `after` parameter were provided to fetchReleases. These parameters cannot be used together.',
); );
}); });
}); });
...@@ -171,7 +110,7 @@ describe('Releases State actions', () => { ...@@ -171,7 +110,7 @@ describe('Releases State actions', () => {
mockedState.sorting.sort = sort; mockedState.sorting.sort = sort;
mockedState.sorting.orderBy = orderBy; mockedState.sorting.orderBy = orderBy;
fetchReleasesGraphQl(vuexParams, { before: undefined, after: undefined }); fetchReleases(vuexParams, { before: undefined, after: undefined });
expect(gqClient.query).toHaveBeenCalledWith({ expect(gqClient.query).toHaveBeenCalledWith({
query: allReleasesQuery, query: allReleasesQuery,
...@@ -191,7 +130,7 @@ describe('Releases State actions', () => { ...@@ -191,7 +130,7 @@ describe('Releases State actions', () => {
const convertedResponse = convertAllReleasesGraphQLResponse(graphqlReleasesResponse); const convertedResponse = convertAllReleasesGraphQLResponse(graphqlReleasesResponse);
return testAction( return testAction(
fetchReleasesGraphQl, fetchReleases,
{}, {},
mockedState, mockedState,
[ [
...@@ -218,90 +157,7 @@ describe('Releases State actions', () => { ...@@ -218,90 +157,7 @@ describe('Releases State actions', () => {
it(`commits ${types.REQUEST_RELEASES} and dispatch receiveReleasesError`, () => { it(`commits ${types.REQUEST_RELEASES} and dispatch receiveReleasesError`, () => {
return testAction( return testAction(
fetchReleasesGraphQl, fetchReleases,
{},
mockedState,
[
{
type: types.REQUEST_RELEASES,
},
],
[
{
type: 'receiveReleasesError',
},
],
);
});
});
});
describe('fetchReleasesRest', () => {
describe('REST query parameters', () => {
let vuexParams;
beforeEach(() => {
jest
.spyOn(api, 'releases')
.mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination });
vuexParams = { dispatch: jest.fn(), commit: jest.fn(), state: mockedState };
});
describe('when a page parameter is provided', () => {
beforeEach(() => {
fetchReleasesRest(vuexParams, { page: 2 });
});
it('makes a REST query with a page query parameter', () => {
expect(api.releases).toHaveBeenCalledWith(projectId, {
page,
order_by: 'released_at',
sort: 'desc',
});
});
});
});
describe('when the request is successful', () => {
beforeEach(() => {
jest
.spyOn(api, 'releases')
.mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination });
});
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),
),
},
},
],
[],
);
});
});
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, mockedState,
[ [
......
...@@ -197,18 +197,6 @@ RSpec.describe 'Milestones through GroupQuery' do ...@@ -197,18 +197,6 @@ RSpec.describe 'Milestones through GroupQuery' do
} }
}) })
end end
context 'when the graphql_milestone_stats feature flag is disabled' do
before do
stub_feature_flags(graphql_milestone_stats: false)
end
it 'returns nil for the stats field' do
expect(post_query).to eq({
'stats' => nil
})
end
end
end end
end end
end end
...@@ -370,23 +370,6 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do ...@@ -370,23 +370,6 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
end end
end end
describe 'ensures that the release data can be contolled by a feature flag' do
context 'when the graphql_release_data feature flag is disabled' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:release) { create(:release, project: project) }
let(:current_user) { developer }
before do
stub_feature_flags(graphql_release_data: false)
project.add_developer(developer)
end
it_behaves_like 'no access to the release field'
end
end
describe 'upcoming release' do describe 'upcoming release' do
let(:path) { path_prefix } let(:path) { path_prefix }
let(:project) { create(:project, :repository, :private) } let(:project) { create(:project, :repository, :private) }
......
...@@ -295,23 +295,6 @@ RSpec.describe 'Query.project(fullPath).releases()' do ...@@ -295,23 +295,6 @@ RSpec.describe 'Query.project(fullPath).releases()' do
end end
end end
describe 'ensures that the release data can be contolled by a feature flag' do
context 'when the graphql_release_data feature flag is disabled' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:release) { create(:release, project: project) }
let(:current_user) { developer }
before do
stub_feature_flags(graphql_release_data: false)
project.add_developer(developer)
end
it_behaves_like 'no access to any release data'
end
end
describe 'sorting behavior' do describe 'sorting behavior' do
let_it_be(:today) { Time.now } let_it_be(:today) { Time.now }
let_it_be(:yesterday) { today - 1.day } let_it_be(:yesterday) { today - 1.day }
......
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