Commit 859681c6 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch 'nfriend-dedicated-release-page' into 'master'

Create dedicated release page for each Release

See merge request gitlab-org/gitlab!24006
parents fa534442 953027b6
import initShowRelease from '~/releases/mount_show';
document.addEventListener('DOMContentLoaded', initShowRelease);
<script>
import { mapState, mapActions } from 'vuex';
import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
import { GlButton, GlLink, GlFormInput, GlFormGroup } from '@gitlab/ui';
import { escape as esc } from 'lodash';
import { __, sprintf } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { BACK_URL_PARAM } from '~/releases/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
export default {
name: 'ReleaseEditApp',
......@@ -12,6 +14,7 @@ export default {
GlFormInput,
GlFormGroup,
GlButton,
GlLink,
MarkdownField,
},
directives: {
......@@ -74,6 +77,9 @@ export default {
this.updateReleaseNotes(notes);
},
},
cancelPath() {
return getParameterByName(BACK_URL_PARAM) || this.releasesPagePath;
},
},
created() {
this.fetchRelease();
......@@ -84,7 +90,6 @@ export default {
'updateRelease',
'updateReleaseTitle',
'updateReleaseNotes',
'navigateToReleasesPage',
]),
},
};
......@@ -157,15 +162,9 @@ export default {
>
{{ __('Save changes') }}
</gl-button>
<gl-button
class="js-cancel-button"
variant="default"
type="button"
:aria-label="__('Cancel')"
@click="navigateToReleasesPage()"
>
<gl-link :href="cancelPath" class="js-cancel-button btn btn-default">
{{ __('Cancel') }}
</gl-button>
</gl-link>
</div>
</form>
</div>
......
<script>
import { mapState, mapActions } from 'vuex';
import { GlSkeletonLoading } from '@gitlab/ui';
import ReleaseBlock from './release_block.vue';
export default {
name: 'ReleaseShowApp',
components: {
GlSkeletonLoading,
ReleaseBlock,
},
computed: {
...mapState('detail', ['isFetchingRelease', 'fetchError', 'release']),
},
created() {
this.fetchRelease();
},
methods: {
...mapActions('detail', ['fetchRelease']),
},
};
</script>
<template>
<div class="prepend-top-default">
<gl-skeleton-loading v-if="isFetchingRelease" />
<release-block v-else-if="!fetchError" :release="release" />
</div>
</template>
<script>
import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
import { setUrlParams } from '~/lib/utils/url_utility';
export default {
name: 'ReleaseBlockHeader',
......@@ -20,7 +22,15 @@ export default {
},
computed: {
editLink() {
return this.release._links?.editUrl;
if (this.release._links?.editUrl) {
const queryParams = {
[BACK_URL_PARAM]: window.location.href,
};
return setUrlParams(queryParams, this.release._links.editUrl);
}
return undefined;
},
selfLink() {
return this.release._links?.self;
......
/* eslint-disable import/prefer-default-export */
// This eslint-disable ^^^ can be removed when at least
// one more constant is added to this file. Currently
// constants.js files with only a single constant
// are flagged by this rule.
export const MAX_MILESTONES_TO_DISPLAY = 5;
export const BACK_URL_PARAM = 'back_url';
......@@ -6,7 +6,15 @@ import detailModule from './stores/modules/detail';
export default () => {
const el = document.getElementById('js-edit-release-page');
const store = createStore({ detail: detailModule });
const store = createStore({
modules: {
detail: detailModule,
},
featureFlags: {
releaseShowPage: Boolean(gon.features?.releaseShowPage),
},
});
store.dispatch('detail/setInitialState', el.dataset);
return new Vue({
......
......@@ -8,7 +8,11 @@ export default () => {
return new Vue({
el,
store: createStore({ list: listModule }),
store: createStore({
modules: {
list: listModule,
},
}),
render: h =>
h(ReleaseListApp, {
props: {
......
import Vue from 'vue';
import ReleaseShowApp from './components/app_show.vue';
import createStore from './stores';
import detailModule from './stores/modules/detail';
export default () => {
const el = document.getElementById('js-show-release-page');
const store = createStore({
modules: {
detail: detailModule,
},
});
store.dispatch('detail/setInitialState', el.dataset);
return new Vue({
el,
store,
render: h => h(ReleaseShowApp),
});
};
......@@ -3,4 +3,8 @@ import Vuex from 'vuex';
Vue.use(Vuex);
export default modules => new Vuex.Store({ modules });
export default ({ modules, featureFlags }) =>
new Vuex.Store({
modules,
state: { featureFlags },
});
......@@ -33,9 +33,11 @@ export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_REL
export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE);
export const receiveUpdateReleaseSuccess = ({ commit, dispatch }) => {
export const receiveUpdateReleaseSuccess = ({ commit, state, rootState }) => {
commit(types.RECEIVE_UPDATE_RELEASE_SUCCESS);
dispatch('navigateToReleasesPage');
redirectTo(
rootState.featureFlags.releaseShowPage ? state.release._links.self : state.releasesPagePath,
);
};
export const receiveUpdateReleaseError = ({ commit }, error) => {
commit(types.RECEIVE_UPDATE_RELEASE_ERROR, error);
......
......@@ -6,22 +6,27 @@ describe 'User edits Release', :js do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:release) { create(:release, project: project, name: 'The first release' ) }
let_it_be(:user) { create(:user) }
let(:show_feature_flag) { true }
before do
stub_feature_flags(release_show_page: show_feature_flag)
project.add_developer(user)
gitlab_sign_in(user)
visit edit_project_release_path(project, release)
wait_for_requests
end
def fill_out_form_and_click(button_to_click)
fill_in 'Release title', with: 'Updated Release title'
fill_in 'Release notes', with: 'Updated Release notes'
click_button button_to_click
click_link_or_button button_to_click
wait_for_requests
wait_for_all_requests
end
it 'renders the breadcrumbs' do
......@@ -42,31 +47,66 @@ describe 'User edits Release', :js do
expect(find_field('Release notes').value).to eq(release.description)
expect(page).to have_button('Save changes')
expect(page).to have_button('Cancel')
expect(page).to have_link('Cancel')
end
it 'redirects to the main Releases page without updating the Release when "Cancel" is clicked' do
it 'does not update the Release when "Cancel" is clicked' do
original_name = release.name
original_description = release.description
fill_out_form_and_click 'Cancel'
expect(current_path).to eq(project_releases_path(project))
release.reload
expect(release.name).to eq(original_name)
expect(release.description).to eq(original_description)
end
it 'updates the Release and redirects to the main Releases page when "Save changes" is clicked' do
it 'updates the Release when "Save changes" is clicked' do
fill_out_form_and_click 'Save changes'
expect(current_path).to eq(project_releases_path(project))
release.reload
expect(release.name).to eq('Updated Release title')
expect(release.description).to eq('Updated Release notes')
end
context 'when the release_show_page feature flag is disabled' do
let(:show_feature_flag) { false }
it 'redirects to the main Releases page when "Cancel" is clicked' do
fill_out_form_and_click 'Cancel'
expect(page).to have_current_path(project_releases_path(project))
end
it 'redirects to the main Releases page when "Save changes" is clicked' do
fill_out_form_and_click 'Save changes'
expect(page).to have_current_path(project_releases_path(project))
end
end
context 'when the release_show_page feature flag is enabled' do
it 'redirects to the previous page when "Cancel" is clicked when the url includes a back_url query parameter' do
back_path = project_releases_path(project, params: { page: 2 })
visit edit_project_release_path(project, release, params: { back_url: back_path })
fill_out_form_and_click 'Cancel'
expect(page).to have_current_path(back_path)
end
it 'redirects to the main Releases page when "Cancel" is clicked when the url does not include a back_url query parameter' do
fill_out_form_and_click 'Cancel'
expect(page).to have_current_path(project_releases_path(project))
end
it 'redirects to the dedicated Release page when "Save changes" is clicked' do
fill_out_form_and_click 'Save changes'
expect(page).to have_current_path(project_release_path(project, release))
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'User views Release', :js do
let(:project) { create(:project, :repository) }
let(:release) { create(:release, project: project, name: 'The first release' ) }
let(:user) { create(:user) }
before do
project.add_developer(user)
gitlab_sign_in(user)
visit project_release_path(project, release)
end
it 'renders the breadcrumbs' do
within('.breadcrumbs') do
expect(page).to have_content("#{project.creator.name} #{project.name} Releases #{release.name}")
expect(page).to have_link(project.creator.name, href: user_path(project.creator))
expect(page).to have_link(project.name, href: project_path(project))
expect(page).to have_link('Releases', href: project_releases_path(project))
expect(page).to have_link(release.name, href: project_release_path(project, release))
end
end
it 'renders the release details' do
within('.release-block') do
expect(page).to have_content(release.name)
expect(page).to have_content(release.tag)
expect(page).to have_content(release.commit.short_id)
expect(page).to have_content(release.description)
end
end
end
import Vuex from 'vuex';
import { mount } from '@vue/test-utils';
import ReleaseEditApp from '~/releases/components/app_edit.vue';
import { release } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { release as originalRelease } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
describe('Release edit component', () => {
let wrapper;
let releaseClone;
let release;
let actions;
let state;
beforeEach(() => {
gon.api_version = 'v4';
releaseClone = convertObjectPropsToCamelCase(release, { deep: true });
const factory = () => {
state = {
release: releaseClone,
release,
markdownDocsPath: 'path/to/markdown/docs',
updateReleaseApiDocsPath: 'path/to/update/release/api/docs',
releasesPagePath: 'path/to/releases/page',
};
actions = {
fetchRelease: jest.fn(),
updateRelease: jest.fn(),
navigateToReleasesPage: jest.fn(),
};
const store = new Vuex.Store({
......@@ -40,58 +37,99 @@ describe('Release edit component', () => {
wrapper = mount(ReleaseEditApp, {
store,
});
};
return wrapper.vm.$nextTick();
});
beforeEach(() => {
gon.api_version = 'v4';
it('calls fetchRelease when the component is created', () => {
expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true });
});
it('renders the description text at the top of the page', () => {
expect(wrapper.find('.js-subtitle-text').text()).toBe(
'Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example v1.0, v2.0-pre.',
);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders the correct tag name in the "Tag name" field', () => {
expect(wrapper.find('#git-ref').element.value).toBe(releaseClone.tagName);
});
describe(`basic functionality tests: all tests unrelated to the "${BACK_URL_PARAM}" parameter`, () => {
beforeEach(() => {
factory();
});
it('renders the correct help text under the "Tag name" field', () => {
const helperText = wrapper.find('#tag-name-help');
const helperTextLink = helperText.find('a');
const helperTextLinkAttrs = helperTextLink.attributes();
expect(helperText.text()).toBe(
'Changing a Release tag is only supported via Releases API. More information',
);
expect(helperTextLink.text()).toBe('More information');
expect(helperTextLinkAttrs.href).toBe(state.updateReleaseApiDocsPath);
expect(helperTextLinkAttrs.rel).toContain('noopener');
expect(helperTextLinkAttrs.rel).toContain('noreferrer');
expect(helperTextLinkAttrs.target).toBe('_blank');
});
it('calls fetchRelease when the component is created', () => {
expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
});
it('renders the correct release title in the "Release title" field', () => {
expect(wrapper.find('#release-title').element.value).toBe(releaseClone.name);
});
it('renders the description text at the top of the page', () => {
expect(wrapper.find('.js-subtitle-text').text()).toBe(
'Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example v1.0, v2.0-pre.',
);
});
it('renders the release notes in the "Release notes" textarea', () => {
expect(wrapper.find('#release-notes').element.value).toBe(releaseClone.description);
});
it('renders the correct tag name in the "Tag name" field', () => {
expect(wrapper.find('#git-ref').element.value).toBe(release.tagName);
});
it('renders the correct help text under the "Tag name" field', () => {
const helperText = wrapper.find('#tag-name-help');
const helperTextLink = helperText.find('a');
const helperTextLinkAttrs = helperTextLink.attributes();
expect(helperText.text()).toBe(
'Changing a Release tag is only supported via Releases API. More information',
);
expect(helperTextLink.text()).toBe('More information');
expect(helperTextLinkAttrs).toEqual(
expect.objectContaining({
href: state.updateReleaseApiDocsPath,
rel: 'noopener noreferrer',
target: '_blank',
}),
);
});
it('renders the correct release title in the "Release title" field', () => {
expect(wrapper.find('#release-title').element.value).toBe(release.name);
});
it('renders the release notes in the "Release notes" textarea', () => {
expect(wrapper.find('#release-notes').element.value).toBe(release.description);
});
it('renders the "Save changes" button as type="submit"', () => {
expect(wrapper.find('.js-submit-button').attributes('type')).toBe('submit');
});
it('renders the "Save changes" button as type="submit"', () => {
expect(wrapper.find('.js-submit-button').attributes('type')).toBe('submit');
it('calls updateRelease when the form is submitted', () => {
wrapper.find('form').trigger('submit');
expect(actions.updateRelease).toHaveBeenCalledTimes(1);
});
});
it('calls updateRelease when the form is submitted', () => {
wrapper.find('form').trigger('submit');
expect(actions.updateRelease).toHaveBeenCalledTimes(1);
describe(`when the URL does not contain a "${BACK_URL_PARAM}" parameter`, () => {
beforeEach(() => {
factory();
});
it(`renders a "Cancel" button with an href pointing to "${BACK_URL_PARAM}"`, () => {
const cancelButton = wrapper.find('.js-cancel-button');
expect(cancelButton.attributes().href).toBe(state.releasesPagePath);
});
});
it('calls navigateToReleasesPage when the "Cancel" button is clicked', () => {
wrapper.find('.js-cancel-button').vm.$emit('click');
expect(actions.navigateToReleasesPage).toHaveBeenCalledTimes(1);
describe(`when the URL contains a "${BACK_URL_PARAM}" parameter`, () => {
const backUrl = 'https://example.gitlab.com/back/url';
beforeEach(() => {
commonUtils.getParameterByName = jest
.fn()
.mockImplementation(paramToGet => ({ [BACK_URL_PARAM]: backUrl }[paramToGet]));
factory();
});
it('renders a "Cancel" button with an href pointing to the main Releases page', () => {
const cancelButton = wrapper.find('.js-cancel-button');
expect(cancelButton.attributes().href).toBe(backUrl);
});
});
});
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import ReleaseShowApp from '~/releases/components/app_show.vue';
import { release as originalRelease } from '../mock_data';
import { GlSkeletonLoading } from '@gitlab/ui';
import ReleaseBlock from '~/releases/components/release_block.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
describe('Release show component', () => {
let wrapper;
let release;
let actions;
beforeEach(() => {
release = convertObjectPropsToCamelCase(originalRelease);
});
const factory = state => {
actions = {
fetchRelease: jest.fn(),
};
const store = new Vuex.Store({
modules: {
detail: {
namespaced: true,
actions,
state,
},
},
});
wrapper = shallowMount(ReleaseShowApp, { store });
};
const findLoadingSkeleton = () => wrapper.find(GlSkeletonLoading);
const findReleaseBlock = () => wrapper.find(ReleaseBlock);
it('calls fetchRelease when the component is created', () => {
factory({ release });
expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
});
it('shows a loading skeleton and hides the release block while the API call is in progress', () => {
factory({ isFetchingRelease: true });
expect(findLoadingSkeleton().exists()).toBe(true);
expect(findReleaseBlock().exists()).toBe(false);
});
it('hides the loading skeleton and shows the release block when the API call finishes successfully', () => {
factory({ isFetchingRelease: false });
expect(findLoadingSkeleton().exists()).toBe(false);
expect(findReleaseBlock().exists()).toBe(true);
});
it('hides both the loading skeleton and the release block when the API call fails', () => {
factory({ fetchError: new Error('Uh oh') });
expect(findLoadingSkeleton().exists()).toBe(false);
expect(findReleaseBlock().exists()).toBe(false);
});
});
......@@ -4,6 +4,7 @@ import { GlLink } from '@gitlab/ui';
import ReleaseBlockHeader from '~/releases/components/release_block_header.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { release as originalRelease } from '../mock_data';
import { BACK_URL_PARAM } from '~/releases/constants';
describe('Release block header', () => {
let wrapper;
......@@ -27,6 +28,7 @@ describe('Release block header', () => {
const findHeader = () => wrapper.find('h2');
const findHeaderLink = () => findHeader().find(GlLink);
const findEditButton = () => wrapper.find('.js-edit-button');
describe('when _links.self is provided', () => {
beforeEach(() => {
......@@ -51,4 +53,39 @@ describe('Release block header', () => {
expect(findHeaderLink().exists()).toBe(false);
});
});
describe('when _links.edit_url is provided', () => {
const currentUrl = 'https://example.gitlab.com/path';
beforeEach(() => {
Object.defineProperty(window, 'location', {
writable: true,
value: {
href: currentUrl,
},
});
factory();
});
it('renders an edit button', () => {
expect(findEditButton().exists()).toBe(true);
});
it('renders the edit button with the correct href', () => {
const expectedQueryParam = `${BACK_URL_PARAM}=${encodeURIComponent(currentUrl)}`;
const expectedUrl = `${release._links.editUrl}?${expectedQueryParam}`;
expect(findEditButton().attributes().href).toBe(expectedUrl);
});
});
describe('when _links.edit is missing', () => {
beforeEach(() => {
factory({ _links: { editUrl: null } });
});
it('does not render an edit button', () => {
expect(findEditButton().exists()).toBe(false);
});
});
});
......@@ -7,20 +7,9 @@ import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { release as originalRelease } from '../mock_data';
import Icon from '~/vue_shared/components/icon.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
const { convertObjectPropsToCamelCase } = jest.requireActual('~/lib/utils/common_utils');
let mockLocationHash;
jest.mock('~/lib/utils/url_utility', () => ({
__esModule: true,
getLocationHash: jest.fn().mockImplementation(() => mockLocationHash),
}));
jest.mock('~/lib/utils/common_utils', () => ({
__esModule: true,
scrollToElement: jest.fn(),
}));
import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
import * as urlUtility from '~/lib/utils/url_utility';
describe('Release block', () => {
let wrapper;
......@@ -47,7 +36,7 @@ describe('Release block', () => {
beforeEach(() => {
jest.spyOn($.fn, 'renderGFM');
release = convertObjectPropsToCamelCase(originalRelease, { deep: true });
release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true });
});
afterEach(() => {
......@@ -61,9 +50,11 @@ describe('Release block', () => {
expect(wrapper.attributes().id).toBe('v0.3');
});
it('renders an edit button that links to the "Edit release" page', () => {
it(`renders an edit button that links to the "Edit release" page with a "${BACK_URL_PARAM}" parameter`, () => {
expect(editButton().exists()).toBe(true);
expect(editButton().attributes('href')).toBe(release._links.editUrl);
expect(editButton().attributes('href')).toBe(
`${release._links.editUrl}?${BACK_URL_PARAM}=${encodeURIComponent(window.location.href)}`,
);
});
it('renders release name', () => {
......@@ -150,14 +141,6 @@ describe('Release block', () => {
});
});
it("does not render an edit button if release._links.editUrl isn't a string", () => {
delete release._links;
return factory(release).then(() => {
expect(editButton().exists()).toBe(false);
});
});
it('does not render the milestone list if no milestones are associated to the release', () => {
delete release.milestones;
......@@ -203,37 +186,40 @@ describe('Release block', () => {
});
describe('anchor scrolling', () => {
let locationHash;
beforeEach(() => {
scrollToElement.mockClear();
commonUtils.scrollToElement = jest.fn();
urlUtility.getLocationHash = jest.fn().mockImplementation(() => locationHash);
});
const hasTargetBlueBackground = () => wrapper.classes('bg-line-target-blue');
it('does not attempt to scroll the page if no anchor tag is included in the URL', () => {
mockLocationHash = '';
locationHash = '';
return factory(release).then(() => {
expect(scrollToElement).not.toHaveBeenCalled();
expect(commonUtils.scrollToElement).not.toHaveBeenCalled();
});
});
it("does not attempt to scroll the page if the anchor tag doesn't match the release's tag name", () => {
mockLocationHash = 'v0.4';
locationHash = 'v0.4';
return factory(release).then(() => {
expect(scrollToElement).not.toHaveBeenCalled();
expect(commonUtils.scrollToElement).not.toHaveBeenCalled();
});
});
it("attempts to scroll itself into view if the anchor tag matches the release's tag name", () => {
mockLocationHash = release.tagName;
locationHash = release.tagName;
return factory(release).then(() => {
expect(scrollToElement).toHaveBeenCalledTimes(1);
expect(commonUtils.scrollToElement).toHaveBeenCalledTimes(1);
expect(scrollToElement).toHaveBeenCalledWith(wrapper.element);
expect(commonUtils.scrollToElement).toHaveBeenCalledWith(wrapper.element);
});
});
it('renders with a light blue background if it is the target of the anchor', () => {
mockLocationHash = release.tagName;
locationHash = release.tagName;
return factory(release).then(() => {
expect(hasTargetBlueBackground()).toBe(true);
......@@ -241,7 +227,7 @@ describe('Release block', () => {
});
it('does not render with a light blue background if it is not the target of the anchor', () => {
mockLocationHash = '';
locationHash = '';
return factory(release).then(() => {
expect(hasTargetBlueBackground()).toBe(false);
......
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { cloneDeep, merge } from 'lodash';
import * as actions from '~/releases/stores/modules/detail/actions';
import * as types from '~/releases/stores/modules/detail/mutation_types';
import { release } from '../../../mock_data';
import state from '~/releases/stores/modules/detail/state';
import { release as originalRelease } from '../../../mock_data';
import createState from '~/releases/stores/modules/detail/state';
import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/flash', () => jest.fn());
......@@ -17,14 +18,14 @@ jest.mock('~/lib/utils/url_utility', () => ({
}));
describe('Release detail actions', () => {
let stateClone;
let releaseClone;
let state;
let release;
let mock;
let error;
beforeEach(() => {
stateClone = state();
releaseClone = JSON.parse(JSON.stringify(release));
state = createState();
release = cloneDeep(originalRelease);
mock = new MockAdapter(axios);
gon.api_version = 'v4';
error = { message: 'An error occurred' };
......@@ -39,7 +40,7 @@ describe('Release detail actions', () => {
it(`commits ${types.SET_INITIAL_STATE} with the provided object`, () => {
const initialState = {};
return testAction(actions.setInitialState, initialState, stateClone, [
return testAction(actions.setInitialState, initialState, state, [
{ type: types.SET_INITIAL_STATE, payload: initialState },
]);
});
......@@ -47,19 +48,19 @@ describe('Release detail actions', () => {
describe('requestRelease', () => {
it(`commits ${types.REQUEST_RELEASE}`, () =>
testAction(actions.requestRelease, undefined, stateClone, [{ type: types.REQUEST_RELEASE }]));
testAction(actions.requestRelease, undefined, state, [{ type: types.REQUEST_RELEASE }]));
});
describe('receiveReleaseSuccess', () => {
it(`commits ${types.RECEIVE_RELEASE_SUCCESS}`, () =>
testAction(actions.receiveReleaseSuccess, releaseClone, stateClone, [
{ type: types.RECEIVE_RELEASE_SUCCESS, payload: releaseClone },
testAction(actions.receiveReleaseSuccess, release, state, [
{ type: types.RECEIVE_RELEASE_SUCCESS, payload: release },
]));
});
describe('receiveReleaseError', () => {
it(`commits ${types.RECEIVE_RELEASE_ERROR}`, () =>
testAction(actions.receiveReleaseError, error, stateClone, [
testAction(actions.receiveReleaseError, error, state, [
{ type: types.RECEIVE_RELEASE_ERROR, payload: error },
]));
......@@ -77,24 +78,24 @@ describe('Release detail actions', () => {
let getReleaseUrl;
beforeEach(() => {
stateClone.projectId = '18';
stateClone.tagName = 'v1.3';
getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`;
state.projectId = '18';
state.tagName = 'v1.3';
getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
});
it(`dispatches requestRelease and receiveReleaseSuccess with the camel-case'd release object`, () => {
mock.onGet(getReleaseUrl).replyOnce(200, releaseClone);
mock.onGet(getReleaseUrl).replyOnce(200, release);
return testAction(
actions.fetchRelease,
undefined,
stateClone,
state,
[],
[
{ type: 'requestRelease' },
{
type: 'receiveReleaseSuccess',
payload: convertObjectPropsToCamelCase(releaseClone, { deep: true }),
payload: convertObjectPropsToCamelCase(release, { deep: true }),
},
],
);
......@@ -106,7 +107,7 @@ describe('Release detail actions', () => {
return testAction(
actions.fetchRelease,
undefined,
stateClone,
state,
[],
[{ type: 'requestRelease' }, { type: 'receiveReleaseError', payload: expect.anything() }],
);
......@@ -116,7 +117,7 @@ describe('Release detail actions', () => {
describe('updateReleaseTitle', () => {
it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => {
const newTitle = 'The new release title';
return testAction(actions.updateReleaseTitle, newTitle, stateClone, [
return testAction(actions.updateReleaseTitle, newTitle, state, [
{ type: types.UPDATE_RELEASE_TITLE, payload: newTitle },
]);
});
......@@ -125,7 +126,7 @@ describe('Release detail actions', () => {
describe('updateReleaseNotes', () => {
it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => {
const newReleaseNotes = 'The new release notes';
return testAction(actions.updateReleaseNotes, newReleaseNotes, stateClone, [
return testAction(actions.updateReleaseNotes, newReleaseNotes, state, [
{ type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes },
]);
});
......@@ -133,25 +134,40 @@ describe('Release detail actions', () => {
describe('requestUpdateRelease', () => {
it(`commits ${types.REQUEST_UPDATE_RELEASE}`, () =>
testAction(actions.requestUpdateRelease, undefined, stateClone, [
testAction(actions.requestUpdateRelease, undefined, state, [
{ type: types.REQUEST_UPDATE_RELEASE },
]));
});
describe('receiveUpdateReleaseSuccess', () => {
it(`commits ${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () =>
testAction(
actions.receiveUpdateReleaseSuccess,
undefined,
stateClone,
[{ type: types.RECEIVE_UPDATE_RELEASE_SUCCESS }],
[{ type: 'navigateToReleasesPage' }],
));
testAction(actions.receiveUpdateReleaseSuccess, undefined, { ...state, featureFlags: {} }, [
{ type: types.RECEIVE_UPDATE_RELEASE_SUCCESS },
]));
describe('when the releaseShowPage feature flag is enabled', () => {
const rootState = { featureFlags: { releaseShowPage: true } };
const updatedState = merge({}, state, {
releasesPagePath: 'path/to/releases/page',
release: {
_links: {
self: 'path/to/self',
},
},
});
actions.receiveUpdateReleaseSuccess({ commit: jest.fn(), state: updatedState, rootState });
expect(redirectTo).toHaveBeenCalledTimes(1);
expect(redirectTo).toHaveBeenCalledWith(updatedState.release._links.self);
});
describe('when the releaseShowPage feature flag is disabled', () => {});
});
describe('receiveUpdateReleaseError', () => {
it(`commits ${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () =>
testAction(actions.receiveUpdateReleaseError, error, stateClone, [
testAction(actions.receiveUpdateReleaseError, error, state, [
{ type: types.RECEIVE_UPDATE_RELEASE_ERROR, payload: error },
]));
......@@ -169,10 +185,10 @@ describe('Release detail actions', () => {
let getReleaseUrl;
beforeEach(() => {
stateClone.release = releaseClone;
stateClone.projectId = '18';
stateClone.tagName = 'v1.3';
getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`;
state.release = release;
state.projectId = '18';
state.tagName = 'v1.3';
getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
});
it(`dispatches requestUpdateRelease and receiveUpdateReleaseSuccess`, () => {
......@@ -181,7 +197,7 @@ describe('Release detail actions', () => {
return testAction(
actions.updateRelease,
undefined,
stateClone,
state,
[],
[{ type: 'requestUpdateRelease' }, { type: 'receiveUpdateReleaseSuccess' }],
);
......@@ -193,7 +209,7 @@ describe('Release detail actions', () => {
return testAction(
actions.updateRelease,
undefined,
stateClone,
state,
[],
[
{ type: 'requestUpdateRelease' },
......@@ -202,16 +218,4 @@ describe('Release detail actions', () => {
);
});
});
describe('navigateToReleasesPage', () => {
it(`calls redirectTo() with the URL to the releases page`, () => {
const releasesPagePath = 'path/to/releases/page';
stateClone.releasesPagePath = releasesPagePath;
actions.navigateToReleasesPage({ state: stateClone });
expect(redirectTo).toHaveBeenCalledTimes(1);
expect(redirectTo).toHaveBeenCalledWith(releasesPagePath);
});
});
});
......@@ -27,7 +27,7 @@ describe('Releases App ', () => {
};
beforeEach(() => {
store = createStore({ list: listModule });
store = createStore({ modules: { list: listModule } });
releasesPagination = _.range(21).map(index => ({
...convertObjectPropsToCamelCase(release, { deep: true }),
tagName: `${index}.00`,
......
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