Commit b0e75592 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch...

Merge branch '229679-migrate-data-toggle-modal-to-glmodal-in-app-assets-javascripts-vue_merge_request_widget-2' into 'master'

[Commit Page] Migrate to GlModal for revert commit

See merge request gitlab-org/gitlab!50522
parents 5c6fdccf ef348707
......@@ -14,6 +14,8 @@ import flash from '~/flash';
import { __ } from '~/locale';
import loadAwardsHandler from '~/awards_handler';
import { initCommitBoxInfo } from '~/projects/commit_box/info';
import initRevertCommitTrigger from '~/projects/commit/init_revert_commit_trigger';
import initRevertCommitModal from '~/projects/commit/init_revert_commit_modal';
const hasPerfBar = document.querySelector('.with-performance-bar');
const performanceHeight = hasPerfBar ? 35 : 0;
......@@ -45,3 +47,5 @@ if (filesContainer.length) {
new Diff();
}
loadAwardsHandler();
initRevertCommitModal();
initRevertCommitTrigger();
<script>
import {
GlDropdown,
GlSearchBoxByType,
GlDropdownItem,
GlDropdownText,
GlLoadingIcon,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { I18N_DROPDOWN } from '../constants';
export default {
name: 'BranchesDropdown',
components: {
GlDropdown,
GlSearchBoxByType,
GlDropdownItem,
GlDropdownText,
GlLoadingIcon,
},
props: {
value: {
type: String,
required: false,
default: '',
},
},
i18n: I18N_DROPDOWN,
data() {
return {
searchTerm: this.value,
};
},
computed: {
...mapGetters(['joinedBranches']),
...mapState(['isFetching', 'branch', 'branches']),
filteredResults() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
return this.joinedBranches.filter((resultString) =>
resultString.toLowerCase().includes(lowerCasedSearchTerm),
);
},
},
mounted() {
this.fetchBranches(this.searchTerm);
},
methods: {
...mapActions(['fetchBranches']),
selectBranch(branch) {
this.$emit('selectBranch', branch);
this.searchTerm = branch; // enables isSelected to work as expected
},
isSelected(selectedBranch) {
return selectedBranch === this.branch;
},
searchTermChanged(value) {
this.searchTerm = value;
this.fetchBranches(value);
},
},
};
</script>
<template>
<gl-dropdown :text="value" :header-text="$options.i18n.headerTitle">
<gl-search-box-by-type
:value="searchTerm"
trim
autocomplete="off"
:debounce="250"
:placeholder="$options.i18n.searchPlaceholder"
@input="searchTermChanged"
/>
<gl-dropdown-item
v-for="branch in filteredResults"
v-show="!isFetching"
:key="branch"
:name="branch"
:is-checked="isSelected(branch)"
is-check-item
@click="selectBranch(branch)"
>
{{ branch }}
</gl-dropdown-item>
<gl-dropdown-text v-show="isFetching" data-testid="dropdown-text-loading-icon">
<gl-loading-icon class="gl-mx-auto" />
</gl-dropdown-text>
<gl-dropdown-text
v-if="!filteredResults.length && !isFetching"
data-testid="empty-result-message"
>
<span class="gl-text-gray-500">{{ $options.i18n.noResultsMessage }}</span>
</gl-dropdown-text>
</gl-dropdown>
</template>
<script>
import { GlModal, GlForm, GlFormCheckbox, GlSprintf, GlFormGroup } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import eventHub from '../event_hub';
import csrf from '~/lib/utils/csrf';
import BranchesDropdown from './branches_dropdown.vue';
export default {
components: {
BranchesDropdown,
GlModal,
GlForm,
GlFormCheckbox,
GlSprintf,
GlFormGroup,
},
inject: {
prependedText: {
default: '',
},
},
props: {
i18n: {
type: Object,
required: true,
},
openModal: {
type: String,
required: true,
},
modalId: {
type: String,
required: true,
},
},
data() {
return {
checked: true,
actionPrimary: {
text: this.i18n.actionPrimaryText,
attributes: [
{ variant: 'success' },
{ category: 'primary' },
{ 'data-testid': 'submit-commit' },
],
},
actionCancel: {
text: this.i18n.actionCancelText,
attributes: [{ 'data-testid': 'cancel-commit' }],
},
};
},
computed: {
...mapState([
'branch',
'endpoint',
'pushCode',
'branchCollaboration',
'modalTitle',
'existingBranch',
'prependedText',
]),
},
mounted() {
eventHub.$on(this.openModal, this.show);
},
methods: {
...mapActions(['clearModal', 'setBranch', 'setSelectedBranch']),
show() {
this.$root.$emit('bv::show::modal', this.modalId);
},
handlePrimary() {
this.$refs.form.$el.submit();
},
resetModalHandler() {
this.clearModal();
this.setSelectedBranch('');
this.checked = true;
},
},
csrf,
};
</script>
<template>
<gl-modal
v-bind="$attrs"
data-testid="modal-commit"
:modal-id="modalId"
size="sm"
:title="modalTitle"
:action-cancel="actionCancel"
:action-primary="actionPrimary"
@hidden="resetModalHandler"
@primary="handlePrimary"
>
<p v-if="prependedText.length" data-testid="prepended-text">
<gl-sprintf :message="prependedText" />
</p>
<gl-form ref="form" :action="endpoint" method="post">
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
<gl-form-group
:label="i18n.branchLabel"
label-for="start_branch"
data-testid="dropdown-group"
>
<input id="start_branch" type="hidden" name="start_branch" :value="branch" />
<branches-dropdown class="gl-w-half" :value="branch" @selectBranch="setBranch" />
</gl-form-group>
<gl-form-checkbox
v-if="pushCode"
v-model="checked"
name="create_merge_request"
class="gl-mt-3"
>
<gl-sprintf :message="i18n.startMergeRequest">
<template #newMergeRequest>
<strong>{{ i18n.newMergeRequest }}</strong>
</template>
</gl-sprintf>
</gl-form-checkbox>
<input v-else type="hidden" name="create_merge_request" value="1" />
</gl-form>
<p v-if="!pushCode" class="gl-mb-0 gl-mt-5" data-testid="appended-text">
<gl-sprintf v-if="branchCollaboration" :message="i18n.existingBranch">
<template #branchName>
<strong>{{ existingBranch }}</strong>
</template>
</gl-sprintf>
<gl-sprintf v-else :message="i18n.branchInFork" />
</p>
</gl-modal>
</template>
<script>
import { GlLink } from '@gitlab/ui';
import eventHub from '../event_hub';
export default {
components: {
GlLink,
},
inject: {
displayText: {
default: '',
},
},
props: {
openModal: {
type: String,
required: true,
},
},
methods: {
showModal() {
eventHub.$emit(this.openModal);
},
},
};
</script>
<template>
<gl-link data-is-link="true" data-testid="revert-commit-link" @click="showModal">
{{ displayText }}
</gl-link>
</template>
import { s__, __ } from '~/locale';
export const OPEN_REVERT_MODAL = 'openRevertModal';
export const REVERT_MODAL_ID = 'revert-commit-modal';
export const I18N_MODAL = {
startMergeRequest: s__('ChangeTypeAction|Start a %{newMergeRequest} with these changes'),
existingBranch: s__(
'ChangeTypeAction|Your changes will be committed to %{branchName} because a merge request is open.',
),
branchInFork: s__(
'ChangeTypeAction|A new branch will be created in your fork and a new merge request will be started.',
),
newMergeRequest: __('new merge request'),
actionCancelText: __('Cancel'),
};
export const I18N_REVERT_MODAL = {
branchLabel: s__('ChangeTypeAction|Revert in branch'),
actionPrimaryText: s__('ChangeTypeAction|Revert'),
};
export const PREPENDED_MODAL_TEXT = s__(
'ChangeTypeAction|This will create a new commit in order to revert the existing changes.',
);
export const I18N_DROPDOWN = {
noResultsMessage: __('No matching results'),
headerTitle: s__('ChangeTypeAction|Switch branch'),
searchPlaceholder: s__('ChangeTypeAction|Search branches'),
};
export const PROJECT_BRANCHES_ERROR = __('Something went wrong while fetching branches');
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();
import Vue from 'vue';
import CommitFormModal from './components/form_modal.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import createStore from './store';
import {
I18N_MODAL,
I18N_REVERT_MODAL,
PREPENDED_MODAL_TEXT,
OPEN_REVERT_MODAL,
REVERT_MODAL_ID,
} from './constants';
export default function initInviteMembersModal() {
const el = document.querySelector('.js-revert-commit-modal');
if (!el) {
return false;
}
const {
title,
endpoint,
branch,
pushCode,
branchCollaboration,
existingBranch,
branchesEndpoint,
} = el.dataset;
const store = createStore({
endpoint,
branchesEndpoint,
branch,
pushCode: parseBoolean(pushCode),
branchCollaboration: parseBoolean(branchCollaboration),
defaultBranch: branch,
modalTitle: title,
existingBranch,
});
return new Vue({
el,
store,
provide: {
prependedText: PREPENDED_MODAL_TEXT,
},
render: (createElement) =>
createElement(CommitFormModal, {
props: {
i18n: { ...I18N_REVERT_MODAL, ...I18N_MODAL },
openModal: OPEN_REVERT_MODAL,
modalId: REVERT_MODAL_ID,
},
}),
});
}
import Vue from 'vue';
import RevertCommitTrigger from './components/form_trigger.vue';
import { OPEN_REVERT_MODAL } from './constants';
export default function initInviteMembersTrigger() {
const el = document.querySelector('.js-revert-commit-trigger');
if (!el) {
return false;
}
const { displayText } = el.dataset;
return new Vue({
el,
provide: { displayText },
render: (createElement) =>
createElement(RevertCommitTrigger, { props: { openModal: OPEN_REVERT_MODAL } }),
});
}
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { PROJECT_BRANCHES_ERROR } from '../constants';
export const clearModal = ({ commit }) => {
commit(types.CLEAR_MODAL);
};
export const requestBranches = ({ commit }) => {
commit(types.REQUEST_BRANCHES);
};
export const fetchBranches = ({ commit, dispatch, state }, query) => {
dispatch('requestBranches');
return axios
.get(state.branchesEndpoint, {
params: { search: query },
})
.then((res) => {
commit(types.RECEIVE_BRANCHES_SUCCESS, res.data);
})
.catch(() => {
createFlash({ message: PROJECT_BRANCHES_ERROR });
});
};
export const setBranch = ({ commit, dispatch }, branch) => {
commit(types.SET_BRANCH, branch);
dispatch('setSelectedBranch', branch);
};
export const setSelectedBranch = ({ commit }, branch) => {
commit(types.SET_SELECTED_BRANCH, branch);
};
import { uniq } from 'lodash';
export const joinedBranches = (state) => {
return uniq(state.branches).sort();
};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
export default (initialState = {}) =>
new Vuex.Store({
actions,
mutations,
getters,
state: {
...state(),
...initialState,
},
});
export const CLEAR_MODAL = 'CLEAR_MODAL';
export const REQUEST_BRANCHES = 'REQUEST_BRANCHES';
export const RECEIVE_BRANCHES_SUCCESS = 'RECEIVE_BRANCHES_SUCCESS';
export const SET_BRANCH = 'SET_BRANCH';
export const SET_SELECTED_BRANCH = 'SET_SELECTED_BRANCH';
import * as types from './mutation_types';
export default {
[types.REQUEST_BRANCHES](state) {
state.isFetching = true;
},
[types.RECEIVE_BRANCHES_SUCCESS](state, branches) {
state.isFetching = false;
state.branches = branches;
state.branches.unshift(state.branch);
},
[types.CLEAR_MODAL](state) {
state.branch = state.defaultBranch;
},
[types.SET_BRANCH](state, branch) {
state.branch = branch;
},
[types.SET_SELECTED_BRANCH](state, branch) {
state.selectedBranch = branch;
},
};
export default () => ({
endpoint: null,
branchesEndpoint: null,
isFetching: false,
branches: [],
selectedBranch: '',
pushCode: false,
branchCollaboration: false,
modalTitle: '',
existingBranch: '',
defaultBranch: '',
branch: '',
});
......@@ -110,8 +110,16 @@ module CommitsHelper
end
end
def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
commit_action_link('revert', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
def revert_commit_link(commit, continue_to_path, btn_class: nil, pajamas: false)
return unless current_user
action = 'revert'
if pajamas && can_collaborate_with_project?(@project)
tag(:div, data: { display_text: action.capitalize }, class: "js-revert-commit-trigger")
else
commit_action_link(action, commit, continue_to_path, btn_class: btn_class, has_tooltip: false)
end
end
def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
......
......@@ -6,34 +6,20 @@
- revert_commit = _('Revert this commit')
- description = s_('ChangeTypeAction|This will create a new commit in order to revert the existing changes.')
- title = commit.merged_merge_request(current_user) ? revert_merge_request : revert_commit
- if defined?(pajamas)
.js-revert-commit-modal{ data: { title: title,
endpoint: revert_namespace_project_commit_path(commit, namespace_id: @project.namespace.full_path, project_id: @project),
branch: @project.default_branch,
push_code: can?(current_user, :push_code, @project).to_s,
branch_collaboration: @project.branch_allows_collaboration?(current_user, selected_branch).to_s,
existing_branch: ERB::Util.html_escape(selected_branch),
branches_endpoint: project_branches_path(@project) } }
- else
= render "projects/commit/commit_modal", title: title, type: type, commit: commit, branch_label: branch_label, description: description, label: label
- when 'cherry-pick'
- label = s_('ChangeTypeAction|Cherry-pick')
- branch_label = s_('ChangeTypeActionLabel|Pick into branch')
- title = commit.merged_merge_request(current_user) ? _('Cherry-pick this merge request') : _('Cherry-pick this commit')
.modal{ id: "modal-#{type}-commit", tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
%h3.page-title= title
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
- if description
%p= description
= form_tag [type.underscore, @project, commit], method: :post, remote: false, class: "js-#{type}-form js-requires-input" do
.form-group.branch
= label_tag 'start_branch', branch_label, class: 'label-bold'
= hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch'
= dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: project_branches_path(@project), submit_form_on_click: false } })
- if can?(current_user, :push_code, @project)
= render 'shared/new_merge_request_checkbox'
- else
= hidden_field_tag 'create_merge_request', 1, id: nil
.form-actions
= submit_tag label, class: 'gl-button btn btn-success'
= link_to _("Cancel"), '#', class: "gl-button btn btn-cancel", "data-dismiss" => "modal"
= render 'shared/projects/edit_information'
= render "projects/commit/commit_modal", title: title, type: type, commit: commit, branch_label: branch_label, description: description, label: label
......@@ -37,7 +37,7 @@
#{ _('Browse Files') }
- if can_collaborate && !@commit.has_been_reverted?(current_user)
%li.clearfix
= revert_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false)
= revert_commit_link(@commit, project_commit_path(@project, @commit.id), pajamas: true)
- if can_collaborate
%li.clearfix
= cherry_pick_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false)
......
.modal{ id: "modal-#{type}-commit", tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
%h3.page-title= title
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
- if description
%p= description
= form_tag [type.underscore, @project, commit], method: :post, remote: false, class: "js-#{type}-form js-requires-input" do
.form-group.branch
= label_tag 'start_branch', branch_label, class: 'label-bold'
= hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch'
= dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: project_branches_path(@project), submit_form_on_click: false } })
- if can?(current_user, :push_code, @project)
= render 'shared/new_merge_request_checkbox'
- else
= hidden_field_tag 'create_merge_request', 1, id: nil
.form-actions
= submit_tag label, class: 'gl-button btn btn-success'
= link_to _("Cancel"), '#', class: "gl-button btn btn-cancel", "data-dismiss" => "modal"
= render 'shared/projects/edit_information'
......@@ -17,5 +17,5 @@
.limited-width-notes
= render "shared/notes/notes_with_form", :autocomplete => true
- if can_collaborate_with_project?(@project)
- %w(revert cherry-pick).each do |type|
= render "projects/commit/change", type: type, commit: @commit, title: @commit.title
= render "projects/commit/change", type: 'revert', commit: @commit, pajamas: true
= render "projects/commit/change", type: 'cherry-pick', commit: @commit, title: @commit.title
---
title: "[Commit Page] Migrate to GlModal for revert commit"
merge_request: 50522
author:
type: other
......@@ -5221,15 +5221,33 @@ msgstr ""
msgid "ChangeTypeActionLabel|Revert in branch"
msgstr ""
msgid "ChangeTypeAction|A new branch will be created in your fork and a new merge request will be started."
msgstr ""
msgid "ChangeTypeAction|Cherry-pick"
msgstr ""
msgid "ChangeTypeAction|Revert"
msgstr ""
msgid "ChangeTypeAction|Revert in branch"
msgstr ""
msgid "ChangeTypeAction|Search branches"
msgstr ""
msgid "ChangeTypeAction|Start a %{newMergeRequest} with these changes"
msgstr ""
msgid "ChangeTypeAction|Switch branch"
msgstr ""
msgid "ChangeTypeAction|This will create a new commit in order to revert the existing changes."
msgstr ""
msgid "ChangeTypeAction|Your changes will be committed to %{branchName} because a merge request is open."
msgstr ""
msgid "Changed assignee(s)."
msgstr ""
......@@ -26176,6 +26194,9 @@ msgstr ""
msgid "Something went wrong while fetching %{listType} list"
msgstr ""
msgid "Something went wrong while fetching branches"
msgstr ""
msgid "Something went wrong while fetching comments. Please try again."
msgstr ""
......
......@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe 'User reverts a commit', :js do
include RepoHelpers
let_it_be(:user) { create(:user) }
let(:project) { create(:project, :repository, namespace: user.namespace) }
let(:user) { create(:user) }
before do
sign_in(user)
......@@ -14,49 +14,37 @@ RSpec.describe 'User reverts a commit', :js do
visit(project_commit_path(project, sample_commit.id))
end
def click_revert
def revert_commit(create_merge_request: false)
find('.header-action-buttons .dropdown').click
find('a[href="#modal-revert-commit"]').click
end
find('[data-testid="revert-commit-link"]').click
context 'without creating a new merge request' do
before do
click_revert
page.within('#modal-revert-commit') do
uncheck('create_merge_request')
click_button('Revert')
end
page.within('[data-testid="modal-commit"]') do
uncheck('create_merge_request') unless create_merge_request
click_button('Revert')
end
end
context 'without creating a new merge request' do
it 'reverts a commit' do
revert_commit
expect(page).to have_content('The commit has been successfully reverted.')
end
it 'does not revert a previously reverted commit' do
revert_commit
# Visit the comment again once it was reverted.
visit project_commit_path(project, sample_commit.id)
find('.header-action-buttons .dropdown').click
find('a[href="#modal-revert-commit"]').click
page.within('#modal-revert-commit') do
uncheck('create_merge_request')
click_button('Revert')
end
revert_commit
expect(page).to have_content('Sorry, we cannot revert this commit automatically.')
end
end
context 'with creating a new merge request' do
before do
click_revert
end
it 'reverts a commit' do
page.within('#modal-revert-commit') do
click_button('Revert')
end
revert_commit(create_merge_request: true)
expect(page).to have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
expect(page).to have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master")
......
import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'jest/helpers/vue_test_utils_helper';
import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue';
Vue.use(Vuex);
describe('BranchesDropdown', () => {
let wrapper;
let store;
const spyFetchBranches = jest.fn();
const createComponent = (term, state = { isFetching: false }) => {
store = new Vuex.Store({
getters: {
joinedBranches: () => ['_master_', '_branch_1_', '_branch_2_'],
},
actions: {
fetchBranches: spyFetchBranches,
},
state,
});
wrapper = extendedWrapper(
shallowMount(BranchesDropdown, {
store,
propsData: {
value: term,
},
}),
);
};
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
const findNoResults = () => wrapper.findByTestId('empty-result-message');
const findLoading = () => wrapper.findByTestId('dropdown-text-loading-icon');
afterEach(() => {
wrapper.destroy();
wrapper = null;
spyFetchBranches.mockReset();
});
describe('On mount', () => {
beforeEach(() => {
createComponent('');
});
it('invokes fetchBranches', () => {
expect(spyFetchBranches).toHaveBeenCalled();
});
});
describe('Loading states', () => {
it('shows loading icon while fetching', () => {
createComponent('', { isFetching: true });
expect(findLoading().isVisible()).toBe(true);
});
it('does not show loading icon', () => {
createComponent('');
expect(findLoading().isVisible()).toBe(false);
});
});
describe('No branches found', () => {
beforeEach(() => {
createComponent('_non_existent_branch_');
});
it('renders empty results message', () => {
expect(findNoResults().text()).toBe('No matching results');
});
it('shows GlSearchBoxByType with default attributes', () => {
expect(findSearchBoxByType().exists()).toBe(true);
expect(findSearchBoxByType().vm.$attrs).toMatchObject({
placeholder: 'Search branches',
debounce: 250,
});
});
});
describe('Search term is empty', () => {
beforeEach(() => {
createComponent('');
});
it('renders all branches when search term is empty', () => {
expect(findAllDropdownItems()).toHaveLength(3);
expect(findDropdownItemByIndex(0).text()).toBe('_master_');
expect(findDropdownItemByIndex(1).text()).toBe('_branch_1_');
expect(findDropdownItemByIndex(2).text()).toBe('_branch_2_');
});
it('should not be selected on the inactive branch', () => {
expect(wrapper.vm.isSelected('_master_')).toBe(false);
});
});
describe('When searching', () => {
beforeEach(() => {
createComponent('');
});
it('invokes fetchBranches', async () => {
const spy = jest.spyOn(wrapper.vm, 'fetchBranches');
findSearchBoxByType().vm.$emit('input', '_anything_');
await wrapper.vm.$nextTick();
expect(spy).toHaveBeenCalledWith('_anything_');
expect(wrapper.vm.searchTerm).toBe('_anything_');
});
});
describe('Branches found', () => {
beforeEach(() => {
createComponent('_branch_1_', { branch: '_branch_1_' });
});
it('renders only the branch searched for', () => {
expect(findAllDropdownItems()).toHaveLength(1);
expect(findDropdownItemByIndex(0).text()).toBe('_branch_1_');
});
it('should not display empty results message', () => {
expect(findNoResults().exists()).toBe(false);
});
it('should signify this branch is selected', () => {
expect(wrapper.vm.isSelected('_branch_1_')).toBe(true);
});
it('should signify the branch is not selected', () => {
expect(wrapper.vm.isSelected('_not_selected_branch_')).toBe(false);
});
describe('Custom events', () => {
it('should emit selectBranch if an branch is clicked', () => {
findDropdownItemByIndex(0).vm.$emit('click');
expect(wrapper.emitted('selectBranch')).toEqual([['_branch_1_']]);
expect(wrapper.vm.searchTerm).toBe('_branch_1_');
});
});
});
describe('Case insensitive for search term', () => {
beforeEach(() => {
createComponent('_BrAnCh_1_');
});
it('renders only the branch searched for', () => {
expect(findAllDropdownItems()).toHaveLength(1);
expect(findDropdownItemByIndex(0).text()).toBe('_branch_1_');
});
});
});
import MockAdapter from 'axios-mock-adapter';
import { shallowMount, mount, createWrapper } from '@vue/test-utils';
import { extendedWrapper } from 'jest/helpers/vue_test_utils_helper';
import { GlModal, GlForm, GlFormCheckbox, GlSprintf } from '@gitlab/ui';
import { within } from '@testing-library/dom';
import axios from '~/lib/utils/axios_utils';
import eventHub from '~/projects/commit/event_hub';
import CommitFormModal from '~/projects/commit/components/form_modal.vue';
import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue';
import createStore from '~/projects/commit/store';
import mockData from '../mock_data';
describe('CommitFormModal', () => {
let wrapper;
let store;
let axiosMock;
const createComponent = (method, state = {}, provide = {}) => {
store = createStore({ ...mockData.mockModal, ...state });
wrapper = extendedWrapper(
method(CommitFormModal, {
provide,
propsData: { ...mockData.modalPropsData },
store,
attrs: {
static: true,
visible: true,
},
}),
);
};
const findModal = () => wrapper.findComponent(GlModal);
const findStartBranch = () => wrapper.find('#start_branch');
const findDropdown = () => wrapper.findComponent(BranchesDropdown);
const findForm = () => findModal().findComponent(GlForm);
const findCheckBox = () => findForm().findComponent(GlFormCheckbox);
const findPrependedText = () => wrapper.findByTestId('prepended-text');
const findAppendedText = () => wrapper.findByTestId('appended-text');
const getByText = (text, options) =>
createWrapper(within(findModal().element).getByText(text, options));
beforeEach(() => {
axiosMock = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
axiosMock.restore();
});
describe('Basic interactions', () => {
it('Listens for opening of modal on mount', () => {
jest.spyOn(eventHub, '$on');
createComponent(shallowMount);
expect(eventHub.$on).toHaveBeenCalledWith(mockData.modalPropsData.openModal, wrapper.vm.show);
});
it('Shows modal', () => {
createComponent(shallowMount);
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
wrapper.vm.show();
expect(rootEmit).toHaveBeenCalledWith('bv::show::modal', mockData.modalPropsData.modalId);
});
it('Clears the modal state once modal is hidden', () => {
createComponent(shallowMount);
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper.vm.checked = false;
findModal().vm.$emit('hidden');
expect(store.dispatch).toHaveBeenCalledWith('clearModal');
expect(store.dispatch).toHaveBeenCalledWith('setSelectedBranch', '');
expect(wrapper.vm.checked).toBe(true);
});
it('Shows the checkbox for new merge request', () => {
createComponent(shallowMount);
expect(findCheckBox().exists()).toBe(true);
});
it('Shows the prepended text', () => {
createComponent(shallowMount, {}, { prependedText: '_prepended_text_' });
expect(findPrependedText().exists()).toBe(true);
expect(findPrependedText().find(GlSprintf).attributes('message')).toBe('_prepended_text_');
});
it('Does not show prepended text', () => {
createComponent(shallowMount);
expect(findPrependedText().exists()).toBe(false);
});
it('Does not show extra message text', () => {
createComponent(shallowMount);
expect(findModal().find('[data-testid="appended-text"]').exists()).toBe(false);
});
it('Does not show the checkbox for new merge request', () => {
createComponent(shallowMount, { pushCode: false });
expect(findCheckBox().exists()).toBe(false);
});
it('Shows the branch in fork message', () => {
createComponent(shallowMount, { pushCode: false });
expect(findAppendedText().exists()).toBe(true);
expect(findAppendedText().find(GlSprintf).attributes('message')).toContain(
mockData.modalPropsData.i18n.branchInFork,
);
});
it('Shows the branch collaboration message', () => {
createComponent(shallowMount, { pushCode: false, branchCollaboration: true });
expect(findAppendedText().exists()).toBe(true);
expect(findAppendedText().find(GlSprintf).attributes('message')).toContain(
mockData.modalPropsData.i18n.existingBranch,
);
});
});
describe('Taking action on the form', () => {
beforeEach(() => {
createComponent(mount);
});
it('Action primary button dispatches submit action', () => {
const submitSpy = jest.spyOn(findForm().element, 'submit');
getByText(mockData.modalPropsData.i18n.actionPrimaryText).trigger('click');
expect(submitSpy).toHaveBeenCalled();
submitSpy.mockRestore();
});
it('Changes the start_branch input value', async () => {
findDropdown().vm.$emit('selectBranch', '_changed_branch_value_');
await wrapper.vm.$nextTick();
expect(findStartBranch().attributes('value')).toBe('_changed_branch_value_');
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import FormTrigger from '~/projects/commit/components/form_trigger.vue';
import eventHub from '~/projects/commit/event_hub';
const displayText = '_display_text_';
const createComponent = () => {
return shallowMount(FormTrigger, {
provide: { displayText },
propsData: { openModal: '_open_modal_' },
});
};
describe('FormTrigger', () => {
let wrapper;
let spy;
beforeEach(() => {
spy = jest.spyOn(eventHub, '$emit');
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findLink = () => wrapper.find(GlLink);
describe('displayText', () => {
it('includes the correct displayText for the link', () => {
expect(findLink().text()).toBe(displayText);
});
});
describe('clicking the link', () => {
it('emits openModal', () => {
findLink().vm.$emit('click');
expect(spy).toHaveBeenCalledWith('_open_modal_');
});
});
});
import { I18N_MODAL } from '~/projects/commit/constants';
export default {
mockModal: {
modalTitle: '_modal_title_',
endpoint: '_endpoint_',
branch: '_branch_',
pushCode: true,
defaultBranch: '_branch_',
existingBranch: '_existing_branch',
branchesEndpoint: '_branches_endpoint_',
},
modalPropsData: {
i18n: {
branchLabel: '_branch_label_',
actionPrimaryText: '_action_primary_text_',
startMergeRequest: '_start_merge_request_',
existingBranch: I18N_MODAL.existingBranch,
branchInFork: '_new_branch_in_fork_message_',
newMergeRequest: '_new merge request_',
actionCancelText: '_action_cancel_text_',
},
modalId: '_modal_id_',
openModal: '_open_modal_',
},
mockBranches: ['_branch_1', '_abc_', '_master_'],
};
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import getInitialState from '~/projects/commit/store/state';
import * as actions from '~/projects/commit/store/actions';
import * as types from '~/projects/commit/store/mutation_types';
import mockData from '../mock_data';
import { PROJECT_BRANCHES_ERROR } from '~/projects/commit/constants';
jest.mock('~/flash.js');
describe('Commit form modal store actions', () => {
let axiosMock;
let state;
beforeEach(() => {
axiosMock = new MockAdapter(axios);
state = getInitialState();
});
afterEach(() => {
axiosMock.restore();
});
describe('clearModal', () => {
it('commits CLEAR_MODAL mutation', () => {
testAction(actions.clearModal, {}, {}, [
{
type: types.CLEAR_MODAL,
},
]);
});
});
describe('requestBranches', () => {
it('commits REQUEST_BRANCHES mutation', () => {
testAction(actions.requestBranches, {}, {}, [
{
type: types.REQUEST_BRANCHES,
},
]);
});
});
describe('fetchBranches', () => {
it('dispatch correct actions on fetchBranches', (done) => {
jest
.spyOn(axios, 'get')
.mockImplementation(() => Promise.resolve({ data: mockData.mockBranches }));
testAction(
actions.fetchBranches,
{},
state,
[
{
type: types.RECEIVE_BRANCHES_SUCCESS,
payload: mockData.mockBranches,
},
],
[{ type: 'requestBranches' }],
() => {
done();
},
);
});
it('should show flash error and set error in state on fetchBranches failure', (done) => {
jest.spyOn(axios, 'get').mockRejectedValue();
testAction(actions.fetchBranches, {}, state, [], [{ type: 'requestBranches' }], () => {
expect(createFlash).toHaveBeenCalledWith({ message: PROJECT_BRANCHES_ERROR });
done();
});
});
});
describe('setBranch', () => {
it('commits SET_BRANCH mutation', () => {
testAction(
actions.setBranch,
{},
{},
[
{
type: types.SET_BRANCH,
payload: {},
},
],
[
{
type: 'setSelectedBranch',
payload: {},
},
],
);
});
});
describe('setSelectedBranch', () => {
it('commits SET_SELECTED_BRANCH mutation', () => {
testAction(actions.setSelectedBranch, {}, {}, [
{
type: types.SET_SELECTED_BRANCH,
payload: {},
},
]);
});
});
});
import * as getters from '~/projects/commit/store/getters';
import mockData from '../mock_data';
describe('Commit form modal getters', () => {
describe('joinedBranches', () => {
it('should join fetched branches with variable branches', () => {
const state = {
branches: mockData.mockBranches,
};
expect(getters.joinedBranches(state)).toEqual(mockData.mockBranches.sort());
});
it('should provide a uniq list of branches', () => {
const branches = ['_branch_', '_branch_', '_different_branch'];
const state = { branches };
expect(getters.joinedBranches(state)).toEqual(branches.slice(1));
});
});
});
import mutations from '~/projects/commit/store/mutations';
import * as types from '~/projects/commit/store/mutation_types';
describe('Commit form modal mutations', () => {
let stateCopy;
describe('REQUEST_BRANCHES', () => {
it('should set isFetching to true', () => {
stateCopy = { isFetching: false };
mutations[types.REQUEST_BRANCHES](stateCopy);
expect(stateCopy.isFetching).toBe(true);
});
});
describe('RECEIVE_BRANCHES_SUCCESS', () => {
it('should set branches', () => {
stateCopy = { branch: '_existing_branch_', isFetching: true };
mutations[types.RECEIVE_BRANCHES_SUCCESS](stateCopy, ['_branch_1_', '_branch_2_']);
expect(stateCopy.branches).toEqual(['_existing_branch_', '_branch_1_', '_branch_2_']);
expect(stateCopy.isFetching).toEqual(false);
});
});
describe('CLEAR_MODAL', () => {
it('should clear modal state ', () => {
stateCopy = { branch: '_master_', defaultBranch: '_default_branch_' };
mutations[types.CLEAR_MODAL](stateCopy);
expect(stateCopy.branch).toEqual('_default_branch_');
});
});
describe('SET_BRANCH', () => {
it('should set branch', () => {
stateCopy = { branch: '_master_' };
mutations[types.SET_BRANCH](stateCopy, '_changed_branch_');
expect(stateCopy.branch).toBe('_changed_branch_');
});
});
describe('SET_SELECTED_BRANCH', () => {
it('should set selectedBranch', () => {
stateCopy = { selectedBranch: '_master_' };
mutations[types.SET_SELECTED_BRANCH](stateCopy, '_changed_branch_');
expect(stateCopy.selectedBranch).toBe('_changed_branch_');
});
});
});
......@@ -3,6 +3,39 @@
require 'spec_helper'
RSpec.describe CommitsHelper do
describe '#revert_commit_link' do
context 'when current_user exists' do
before do
allow(helper).to receive(:current_user).and_return(double('User'))
allow(helper).to receive(:can_collaborate_with_project?).and_return(true)
end
it 'renders a div for Vue' do
result = helper.revert_commit_link('_commit_', '_path_', pajamas: true)
expect(result).to include('js-revert-commit-trigger')
end
it 'does not render a div for Vue' do
result = helper.revert_commit_link('_commit_', '_path_')
expect(result).not_to include('js-revert-commit-trigger')
end
end
context 'when current_user does not exist' do
before do
allow(helper).to receive(:current_user).and_return(nil)
end
it 'does not render anything' do
result = helper.revert_commit_link(double('Commit'), '_path_')
expect(result).to be_nil
end
end
end
describe 'commit_author_link' do
it 'escapes the author email' do
commit = double(
......
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