Commit 6958c41a authored by Nathan Friend's avatar Nathan Friend

Update redirect behavior of Edit Release buttons

This commit updates the redirect behavior of the buttons at the bottom
of the "Edit Release" page.
parent fdd6fc01
<script> <script>
import { mapState, mapActions } from 'vuex'; 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 { escape as esc } from 'lodash';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { BACK_URL_PARAM } from '~/releases/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
export default { export default {
name: 'ReleaseEditApp', name: 'ReleaseEditApp',
...@@ -12,6 +14,7 @@ export default { ...@@ -12,6 +14,7 @@ export default {
GlFormInput, GlFormInput,
GlFormGroup, GlFormGroup,
GlButton, GlButton,
GlLink,
MarkdownField, MarkdownField,
}, },
directives: { directives: {
...@@ -74,6 +77,9 @@ export default { ...@@ -74,6 +77,9 @@ export default {
this.updateReleaseNotes(notes); this.updateReleaseNotes(notes);
}, },
}, },
cancelPath() {
return getParameterByName(BACK_URL_PARAM) || this.releasesPagePath;
},
}, },
created() { created() {
this.fetchRelease(); this.fetchRelease();
...@@ -84,7 +90,6 @@ export default { ...@@ -84,7 +90,6 @@ export default {
'updateRelease', 'updateRelease',
'updateReleaseTitle', 'updateReleaseTitle',
'updateReleaseNotes', 'updateReleaseNotes',
'navigateToReleasesPage',
]), ]),
}, },
}; };
...@@ -157,15 +162,9 @@ export default { ...@@ -157,15 +162,9 @@ export default {
> >
{{ __('Save changes') }} {{ __('Save changes') }}
</gl-button> </gl-button>
<gl-button <gl-link :href="cancelPath" class="js-cancel-button btn btn-default">
class="js-cancel-button"
variant="default"
type="button"
:aria-label="__('Cancel')"
@click="navigateToReleasesPage()"
>
{{ __('Cancel') }} {{ __('Cancel') }}
</gl-button> </gl-link>
</div> </div>
</form> </form>
</div> </div>
......
<script> <script>
import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui'; import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
import { setUrlParams } from '~/lib/utils/url_utility';
export default { export default {
name: 'ReleaseBlockHeader', name: 'ReleaseBlockHeader',
...@@ -20,7 +22,15 @@ export default { ...@@ -20,7 +22,15 @@ export default {
}, },
computed: { computed: {
editLink() { 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() { selfLink() {
return this.release._links?.self; 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 MAX_MILESTONES_TO_DISPLAY = 5;
export const BACK_URL_PARAM = 'back_url';
...@@ -6,7 +6,15 @@ import detailModule from './stores/modules/detail'; ...@@ -6,7 +6,15 @@ import detailModule from './stores/modules/detail';
export default () => { export default () => {
const el = document.getElementById('js-edit-release-page'); 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); store.dispatch('detail/setInitialState', el.dataset);
return new Vue({ return new Vue({
......
...@@ -8,7 +8,11 @@ export default () => { ...@@ -8,7 +8,11 @@ export default () => {
return new Vue({ return new Vue({
el, el,
store: createStore({ list: listModule }), store: createStore({
modules: {
list: listModule,
},
}),
render: h => render: h =>
h(ReleaseListApp, { h(ReleaseListApp, {
props: { props: {
......
...@@ -6,7 +6,11 @@ import detailModule from './stores/modules/detail'; ...@@ -6,7 +6,11 @@ import detailModule from './stores/modules/detail';
export default () => { export default () => {
const el = document.getElementById('js-show-release-page'); const el = document.getElementById('js-show-release-page');
const store = createStore({ detail: detailModule }); const store = createStore({
modules: {
detail: detailModule,
},
});
store.dispatch('detail/setInitialState', el.dataset); store.dispatch('detail/setInitialState', el.dataset);
return new Vue({ return new Vue({
......
...@@ -3,4 +3,8 @@ import Vuex from 'vuex'; ...@@ -3,4 +3,8 @@ import Vuex from 'vuex';
Vue.use(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 ...@@ -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 updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE); 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); commit(types.RECEIVE_UPDATE_RELEASE_SUCCESS);
dispatch('navigateToReleasesPage'); redirectTo(
rootState.featureFlags.releaseShowPage ? state.release._links.self : state.releasesPagePath,
);
}; };
export const receiveUpdateReleaseError = ({ commit }, error) => { export const receiveUpdateReleaseError = ({ commit }, error) => {
commit(types.RECEIVE_UPDATE_RELEASE_ERROR, error); commit(types.RECEIVE_UPDATE_RELEASE_ERROR, error);
......
...@@ -6,22 +6,27 @@ describe 'User edits Release', :js do ...@@ -6,22 +6,27 @@ describe 'User edits Release', :js do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:release) { create(:release, project: project, name: 'The first release' ) } let_it_be(:release) { create(:release, project: project, name: 'The first release' ) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:show_feature_flag) { true }
before do before do
stub_feature_flags(release_show_page: show_feature_flag)
project.add_developer(user) project.add_developer(user)
gitlab_sign_in(user) gitlab_sign_in(user)
visit edit_project_release_path(project, release) visit edit_project_release_path(project, release)
wait_for_requests
end end
def fill_out_form_and_click(button_to_click) def fill_out_form_and_click(button_to_click)
fill_in 'Release title', with: 'Updated Release title' fill_in 'Release title', with: 'Updated Release title'
fill_in 'Release notes', with: 'Updated Release notes' 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 end
it 'renders the breadcrumbs' do it 'renders the breadcrumbs' do
...@@ -42,31 +47,66 @@ describe 'User edits Release', :js do ...@@ -42,31 +47,66 @@ describe 'User edits Release', :js do
expect(find_field('Release notes').value).to eq(release.description) expect(find_field('Release notes').value).to eq(release.description)
expect(page).to have_button('Save changes') expect(page).to have_button('Save changes')
expect(page).to have_button('Cancel') expect(page).to have_link('Cancel')
end 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_name = release.name
original_description = release.description original_description = release.description
fill_out_form_and_click 'Cancel' fill_out_form_and_click 'Cancel'
expect(current_path).to eq(project_releases_path(project))
release.reload release.reload
expect(release.name).to eq(original_name) expect(release.name).to eq(original_name)
expect(release.description).to eq(original_description) expect(release.description).to eq(original_description)
end 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' fill_out_form_and_click 'Save changes'
expect(current_path).to eq(project_releases_path(project))
release.reload release.reload
expect(release.name).to eq('Updated Release title') expect(release.name).to eq('Updated Release title')
expect(release.description).to eq('Updated Release notes') expect(release.description).to eq('Updated Release notes')
end 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 end
import Vuex from 'vuex'; import Vuex from 'vuex';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import ReleaseEditApp from '~/releases/components/app_edit.vue'; import ReleaseEditApp from '~/releases/components/app_edit.vue';
import { release } from '../mock_data'; import { release as originalRelease } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
describe('Release edit component', () => { describe('Release edit component', () => {
let wrapper; let wrapper;
let releaseClone; let release;
let actions; let actions;
let state; let state;
beforeEach(() => { const factory = () => {
gon.api_version = 'v4';
releaseClone = convertObjectPropsToCamelCase(release, { deep: true });
state = { state = {
release: releaseClone, release,
markdownDocsPath: 'path/to/markdown/docs', markdownDocsPath: 'path/to/markdown/docs',
updateReleaseApiDocsPath: 'path/to/update/release/api/docs', updateReleaseApiDocsPath: 'path/to/update/release/api/docs',
releasesPagePath: 'path/to/releases/page',
}; };
actions = { actions = {
fetchRelease: jest.fn(), fetchRelease: jest.fn(),
updateRelease: jest.fn(), updateRelease: jest.fn(),
navigateToReleasesPage: jest.fn(),
}; };
const store = new Vuex.Store({ const store = new Vuex.Store({
...@@ -40,58 +37,99 @@ describe('Release edit component', () => { ...@@ -40,58 +37,99 @@ describe('Release edit component', () => {
wrapper = mount(ReleaseEditApp, { wrapper = mount(ReleaseEditApp, {
store, store,
}); });
};
return wrapper.vm.$nextTick(); beforeEach(() => {
}); gon.api_version = 'v4';
it('calls fetchRelease when the component is created', () => { release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true });
expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
}); });
it('renders the description text at the top of the page', () => { afterEach(() => {
expect(wrapper.find('.js-subtitle-text').text()).toBe( wrapper.destroy();
'Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example v1.0, v2.0-pre.', wrapper = null;
);
}); });
it('renders the correct tag name in the "Tag name" field', () => { describe(`basic functionality tests: all tests unrelated to the "${BACK_URL_PARAM}" parameter`, () => {
expect(wrapper.find('#git-ref').element.value).toBe(releaseClone.tagName); beforeEach(() => {
}); factory();
});
it('renders the correct help text under the "Tag name" field', () => { it('calls fetchRelease when the component is created', () => {
const helperText = wrapper.find('#tag-name-help'); expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
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('renders the correct release title in the "Release title" field', () => { it('renders the description text at the top of the page', () => {
expect(wrapper.find('#release-title').element.value).toBe(releaseClone.name); 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', () => { it('renders the correct tag name in the "Tag name" field', () => {
expect(wrapper.find('#release-notes').element.value).toBe(releaseClone.description); 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"', () => { it('calls updateRelease when the form is submitted', () => {
expect(wrapper.find('.js-submit-button').attributes('type')).toBe('submit'); wrapper.find('form').trigger('submit');
expect(actions.updateRelease).toHaveBeenCalledTimes(1);
});
}); });
it('calls updateRelease when the form is submitted', () => { describe(`when the URL does not contain a "${BACK_URL_PARAM}" parameter`, () => {
wrapper.find('form').trigger('submit'); beforeEach(() => {
expect(actions.updateRelease).toHaveBeenCalledTimes(1); 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', () => { describe(`when the URL contains a "${BACK_URL_PARAM}" parameter`, () => {
wrapper.find('.js-cancel-button').vm.$emit('click'); const backUrl = 'https://example.gitlab.com/back/url';
expect(actions.navigateToReleasesPage).toHaveBeenCalledTimes(1);
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);
});
}); });
}); });
...@@ -4,6 +4,7 @@ import { GlLink } from '@gitlab/ui'; ...@@ -4,6 +4,7 @@ import { GlLink } from '@gitlab/ui';
import ReleaseBlockHeader from '~/releases/components/release_block_header.vue'; import ReleaseBlockHeader from '~/releases/components/release_block_header.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { release as originalRelease } from '../mock_data'; import { release as originalRelease } from '../mock_data';
import { BACK_URL_PARAM } from '~/releases/constants';
describe('Release block header', () => { describe('Release block header', () => {
let wrapper; let wrapper;
...@@ -27,6 +28,7 @@ describe('Release block header', () => { ...@@ -27,6 +28,7 @@ describe('Release block header', () => {
const findHeader = () => wrapper.find('h2'); const findHeader = () => wrapper.find('h2');
const findHeaderLink = () => findHeader().find(GlLink); const findHeaderLink = () => findHeader().find(GlLink);
const findEditButton = () => wrapper.find('.js-edit-button');
describe('when _links.self is provided', () => { describe('when _links.self is provided', () => {
beforeEach(() => { beforeEach(() => {
...@@ -51,4 +53,39 @@ describe('Release block header', () => { ...@@ -51,4 +53,39 @@ describe('Release block header', () => {
expect(findHeaderLink().exists()).toBe(false); 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'; ...@@ -7,20 +7,9 @@ import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { release as originalRelease } from '../mock_data'; import { release as originalRelease } from '../mock_data';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { scrollToElement } from '~/lib/utils/common_utils'; import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
const { convertObjectPropsToCamelCase } = jest.requireActual('~/lib/utils/common_utils'); import * as urlUtility from '~/lib/utils/url_utility';
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(),
}));
describe('Release block', () => { describe('Release block', () => {
let wrapper; let wrapper;
...@@ -47,7 +36,7 @@ describe('Release block', () => { ...@@ -47,7 +36,7 @@ describe('Release block', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn($.fn, 'renderGFM'); jest.spyOn($.fn, 'renderGFM');
release = convertObjectPropsToCamelCase(originalRelease, { deep: true }); release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true });
}); });
afterEach(() => { afterEach(() => {
...@@ -61,9 +50,11 @@ describe('Release block', () => { ...@@ -61,9 +50,11 @@ describe('Release block', () => {
expect(wrapper.attributes().id).toBe('v0.3'); 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().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', () => { it('renders release name', () => {
...@@ -150,14 +141,6 @@ describe('Release block', () => { ...@@ -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', () => { it('does not render the milestone list if no milestones are associated to the release', () => {
delete release.milestones; delete release.milestones;
...@@ -203,37 +186,40 @@ describe('Release block', () => { ...@@ -203,37 +186,40 @@ describe('Release block', () => {
}); });
describe('anchor scrolling', () => { describe('anchor scrolling', () => {
let locationHash;
beforeEach(() => { beforeEach(() => {
scrollToElement.mockClear(); commonUtils.scrollToElement = jest.fn();
urlUtility.getLocationHash = jest.fn().mockImplementation(() => locationHash);
}); });
const hasTargetBlueBackground = () => wrapper.classes('bg-line-target-blue'); 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', () => { it('does not attempt to scroll the page if no anchor tag is included in the URL', () => {
mockLocationHash = ''; locationHash = '';
return factory(release).then(() => { 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", () => { 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(() => { 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", () => { 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(() => { 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', () => { 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(() => { return factory(release).then(() => {
expect(hasTargetBlueBackground()).toBe(true); expect(hasTargetBlueBackground()).toBe(true);
...@@ -241,7 +227,7 @@ describe('Release block', () => { ...@@ -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', () => { it('does not render with a light blue background if it is not the target of the anchor', () => {
mockLocationHash = ''; locationHash = '';
return factory(release).then(() => { return factory(release).then(() => {
expect(hasTargetBlueBackground()).toBe(false); expect(hasTargetBlueBackground()).toBe(false);
......
import axios from 'axios'; import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import { cloneDeep, merge } from 'lodash';
import * as actions from '~/releases/stores/modules/detail/actions'; import * as actions from '~/releases/stores/modules/detail/actions';
import * as types from '~/releases/stores/modules/detail/mutation_types'; import * as types from '~/releases/stores/modules/detail/mutation_types';
import { release } from '../../../mock_data'; import { release as originalRelease } from '../../../mock_data';
import state from '~/releases/stores/modules/detail/state'; import createState from '~/releases/stores/modules/detail/state';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/flash', () => jest.fn()); jest.mock('~/flash', () => jest.fn());
...@@ -17,14 +18,14 @@ jest.mock('~/lib/utils/url_utility', () => ({ ...@@ -17,14 +18,14 @@ jest.mock('~/lib/utils/url_utility', () => ({
})); }));
describe('Release detail actions', () => { describe('Release detail actions', () => {
let stateClone; let state;
let releaseClone; let release;
let mock; let mock;
let error; let error;
beforeEach(() => { beforeEach(() => {
stateClone = state(); state = createState();
releaseClone = JSON.parse(JSON.stringify(release)); release = cloneDeep(originalRelease);
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
gon.api_version = 'v4'; gon.api_version = 'v4';
error = { message: 'An error occurred' }; error = { message: 'An error occurred' };
...@@ -39,7 +40,7 @@ describe('Release detail actions', () => { ...@@ -39,7 +40,7 @@ describe('Release detail actions', () => {
it(`commits ${types.SET_INITIAL_STATE} with the provided object`, () => { it(`commits ${types.SET_INITIAL_STATE} with the provided object`, () => {
const initialState = {}; const initialState = {};
return testAction(actions.setInitialState, initialState, stateClone, [ return testAction(actions.setInitialState, initialState, state, [
{ type: types.SET_INITIAL_STATE, payload: initialState }, { type: types.SET_INITIAL_STATE, payload: initialState },
]); ]);
}); });
...@@ -47,19 +48,19 @@ describe('Release detail actions', () => { ...@@ -47,19 +48,19 @@ describe('Release detail actions', () => {
describe('requestRelease', () => { describe('requestRelease', () => {
it(`commits ${types.REQUEST_RELEASE}`, () => 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', () => { describe('receiveReleaseSuccess', () => {
it(`commits ${types.RECEIVE_RELEASE_SUCCESS}`, () => it(`commits ${types.RECEIVE_RELEASE_SUCCESS}`, () =>
testAction(actions.receiveReleaseSuccess, releaseClone, stateClone, [ testAction(actions.receiveReleaseSuccess, release, state, [
{ type: types.RECEIVE_RELEASE_SUCCESS, payload: releaseClone }, { type: types.RECEIVE_RELEASE_SUCCESS, payload: release },
])); ]));
}); });
describe('receiveReleaseError', () => { describe('receiveReleaseError', () => {
it(`commits ${types.RECEIVE_RELEASE_ERROR}`, () => it(`commits ${types.RECEIVE_RELEASE_ERROR}`, () =>
testAction(actions.receiveReleaseError, error, stateClone, [ testAction(actions.receiveReleaseError, error, state, [
{ type: types.RECEIVE_RELEASE_ERROR, payload: error }, { type: types.RECEIVE_RELEASE_ERROR, payload: error },
])); ]));
...@@ -77,24 +78,24 @@ describe('Release detail actions', () => { ...@@ -77,24 +78,24 @@ describe('Release detail actions', () => {
let getReleaseUrl; let getReleaseUrl;
beforeEach(() => { beforeEach(() => {
stateClone.projectId = '18'; state.projectId = '18';
stateClone.tagName = 'v1.3'; state.tagName = 'v1.3';
getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`; getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
}); });
it(`dispatches requestRelease and receiveReleaseSuccess with the camel-case'd release object`, () => { 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( return testAction(
actions.fetchRelease, actions.fetchRelease,
undefined, undefined,
stateClone, state,
[], [],
[ [
{ type: 'requestRelease' }, { type: 'requestRelease' },
{ {
type: 'receiveReleaseSuccess', type: 'receiveReleaseSuccess',
payload: convertObjectPropsToCamelCase(releaseClone, { deep: true }), payload: convertObjectPropsToCamelCase(release, { deep: true }),
}, },
], ],
); );
...@@ -106,7 +107,7 @@ describe('Release detail actions', () => { ...@@ -106,7 +107,7 @@ describe('Release detail actions', () => {
return testAction( return testAction(
actions.fetchRelease, actions.fetchRelease,
undefined, undefined,
stateClone, state,
[], [],
[{ type: 'requestRelease' }, { type: 'receiveReleaseError', payload: expect.anything() }], [{ type: 'requestRelease' }, { type: 'receiveReleaseError', payload: expect.anything() }],
); );
...@@ -116,7 +117,7 @@ describe('Release detail actions', () => { ...@@ -116,7 +117,7 @@ describe('Release detail actions', () => {
describe('updateReleaseTitle', () => { describe('updateReleaseTitle', () => {
it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => { it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => {
const newTitle = 'The new 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 }, { type: types.UPDATE_RELEASE_TITLE, payload: newTitle },
]); ]);
}); });
...@@ -125,7 +126,7 @@ describe('Release detail actions', () => { ...@@ -125,7 +126,7 @@ describe('Release detail actions', () => {
describe('updateReleaseNotes', () => { describe('updateReleaseNotes', () => {
it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => { it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => {
const newReleaseNotes = 'The new 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 }, { type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes },
]); ]);
}); });
...@@ -133,25 +134,40 @@ describe('Release detail actions', () => { ...@@ -133,25 +134,40 @@ describe('Release detail actions', () => {
describe('requestUpdateRelease', () => { describe('requestUpdateRelease', () => {
it(`commits ${types.REQUEST_UPDATE_RELEASE}`, () => it(`commits ${types.REQUEST_UPDATE_RELEASE}`, () =>
testAction(actions.requestUpdateRelease, undefined, stateClone, [ testAction(actions.requestUpdateRelease, undefined, state, [
{ type: types.REQUEST_UPDATE_RELEASE }, { type: types.REQUEST_UPDATE_RELEASE },
])); ]));
}); });
describe('receiveUpdateReleaseSuccess', () => { describe('receiveUpdateReleaseSuccess', () => {
it(`commits ${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () => it(`commits ${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () =>
testAction( testAction(actions.receiveUpdateReleaseSuccess, undefined, { ...state, featureFlags: {} }, [
actions.receiveUpdateReleaseSuccess, { type: types.RECEIVE_UPDATE_RELEASE_SUCCESS },
undefined, ]));
stateClone,
[{ type: types.RECEIVE_UPDATE_RELEASE_SUCCESS }], describe('when the releaseShowPage feature flag is enabled', () => {
[{ type: 'navigateToReleasesPage' }], 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', () => { describe('receiveUpdateReleaseError', () => {
it(`commits ${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () => 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 }, { type: types.RECEIVE_UPDATE_RELEASE_ERROR, payload: error },
])); ]));
...@@ -169,10 +185,10 @@ describe('Release detail actions', () => { ...@@ -169,10 +185,10 @@ describe('Release detail actions', () => {
let getReleaseUrl; let getReleaseUrl;
beforeEach(() => { beforeEach(() => {
stateClone.release = releaseClone; state.release = release;
stateClone.projectId = '18'; state.projectId = '18';
stateClone.tagName = 'v1.3'; state.tagName = 'v1.3';
getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`; getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
}); });
it(`dispatches requestUpdateRelease and receiveUpdateReleaseSuccess`, () => { it(`dispatches requestUpdateRelease and receiveUpdateReleaseSuccess`, () => {
...@@ -181,7 +197,7 @@ describe('Release detail actions', () => { ...@@ -181,7 +197,7 @@ describe('Release detail actions', () => {
return testAction( return testAction(
actions.updateRelease, actions.updateRelease,
undefined, undefined,
stateClone, state,
[], [],
[{ type: 'requestUpdateRelease' }, { type: 'receiveUpdateReleaseSuccess' }], [{ type: 'requestUpdateRelease' }, { type: 'receiveUpdateReleaseSuccess' }],
); );
...@@ -193,7 +209,7 @@ describe('Release detail actions', () => { ...@@ -193,7 +209,7 @@ describe('Release detail actions', () => {
return testAction( return testAction(
actions.updateRelease, actions.updateRelease,
undefined, undefined,
stateClone, state,
[], [],
[ [
{ type: 'requestUpdateRelease' }, { type: 'requestUpdateRelease' },
...@@ -202,16 +218,4 @@ describe('Release detail actions', () => { ...@@ -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 ', () => { ...@@ -27,7 +27,7 @@ describe('Releases App ', () => {
}; };
beforeEach(() => { beforeEach(() => {
store = createStore({ list: listModule }); store = createStore({ modules: { list: listModule } });
releasesPagination = _.range(21).map(index => ({ releasesPagination = _.range(21).map(index => ({
...convertObjectPropsToCamelCase(release, { deep: true }), ...convertObjectPropsToCamelCase(release, { deep: true }),
tagName: `${index}.00`, 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