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'; ...@@ -14,6 +14,8 @@ import flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import loadAwardsHandler from '~/awards_handler'; import loadAwardsHandler from '~/awards_handler';
import { initCommitBoxInfo } from '~/projects/commit_box/info'; 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 hasPerfBar = document.querySelector('.with-performance-bar');
const performanceHeight = hasPerfBar ? 35 : 0; const performanceHeight = hasPerfBar ? 35 : 0;
...@@ -45,3 +47,5 @@ if (filesContainer.length) { ...@@ -45,3 +47,5 @@ if (filesContainer.length) {
new Diff(); new Diff();
} }
loadAwardsHandler(); 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 ...@@ -110,8 +110,16 @@ module CommitsHelper
end end
end end
def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) def revert_commit_link(commit, continue_to_path, btn_class: nil, pajamas: false)
commit_action_link('revert', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip) 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 end
def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
......
...@@ -6,34 +6,20 @@ ...@@ -6,34 +6,20 @@
- revert_commit = _('Revert this commit') - revert_commit = _('Revert this commit')
- description = s_('ChangeTypeAction|This will create a new commit in order to revert the existing changes.') - 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 - 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' - when 'cherry-pick'
- label = s_('ChangeTypeAction|Cherry-pick') - label = s_('ChangeTypeAction|Cherry-pick')
- branch_label = s_('ChangeTypeActionLabel|Pick into branch') - branch_label = s_('ChangeTypeActionLabel|Pick into branch')
- title = commit.merged_merge_request(current_user) ? _('Cherry-pick this merge request') : _('Cherry-pick this commit') - title = commit.merged_merge_request(current_user) ? _('Cherry-pick this merge request') : _('Cherry-pick this commit')
= render "projects/commit/commit_modal", title: title, type: type, commit: commit, branch_label: branch_label, description: description, label: label
.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'
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
#{ _('Browse Files') } #{ _('Browse Files') }
- if can_collaborate && !@commit.has_been_reverted?(current_user) - if can_collaborate && !@commit.has_been_reverted?(current_user)
%li.clearfix %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 - if can_collaborate
%li.clearfix %li.clearfix
= cherry_pick_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false) = 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 @@ ...@@ -17,5 +17,5 @@
.limited-width-notes .limited-width-notes
= render "shared/notes/notes_with_form", :autocomplete => true = render "shared/notes/notes_with_form", :autocomplete => true
- if can_collaborate_with_project?(@project) - if can_collaborate_with_project?(@project)
- %w(revert cherry-pick).each do |type| = render "projects/commit/change", type: 'revert', commit: @commit, pajamas: true
= render "projects/commit/change", type: type, commit: @commit, title: @commit.title = 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 "" ...@@ -5221,15 +5221,33 @@ msgstr ""
msgid "ChangeTypeActionLabel|Revert in branch" msgid "ChangeTypeActionLabel|Revert in branch"
msgstr "" 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" msgid "ChangeTypeAction|Cherry-pick"
msgstr "" msgstr ""
msgid "ChangeTypeAction|Revert" msgid "ChangeTypeAction|Revert"
msgstr "" 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." msgid "ChangeTypeAction|This will create a new commit in order to revert the existing changes."
msgstr "" msgstr ""
msgid "ChangeTypeAction|Your changes will be committed to %{branchName} because a merge request is open."
msgstr ""
msgid "Changed assignee(s)." msgid "Changed assignee(s)."
msgstr "" msgstr ""
...@@ -26176,6 +26194,9 @@ msgstr "" ...@@ -26176,6 +26194,9 @@ msgstr ""
msgid "Something went wrong while fetching %{listType} list" msgid "Something went wrong while fetching %{listType} list"
msgstr "" msgstr ""
msgid "Something went wrong while fetching branches"
msgstr ""
msgid "Something went wrong while fetching comments. Please try again." msgid "Something went wrong while fetching comments. Please try again."
msgstr "" msgstr ""
......
...@@ -5,8 +5,8 @@ require 'spec_helper' ...@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe 'User reverts a commit', :js do RSpec.describe 'User reverts a commit', :js do
include RepoHelpers include RepoHelpers
let_it_be(:user) { create(:user) }
let(:project) { create(:project, :repository, namespace: user.namespace) } let(:project) { create(:project, :repository, namespace: user.namespace) }
let(:user) { create(:user) }
before do before do
sign_in(user) sign_in(user)
...@@ -14,49 +14,37 @@ RSpec.describe 'User reverts a commit', :js do ...@@ -14,49 +14,37 @@ RSpec.describe 'User reverts a commit', :js do
visit(project_commit_path(project, sample_commit.id)) visit(project_commit_path(project, sample_commit.id))
end end
def click_revert def revert_commit(create_merge_request: false)
find('.header-action-buttons .dropdown').click find('.header-action-buttons .dropdown').click
find('a[href="#modal-revert-commit"]').click find('[data-testid="revert-commit-link"]').click
end
context 'without creating a new merge request' do page.within('[data-testid="modal-commit"]') do
before do uncheck('create_merge_request') unless create_merge_request
click_revert click_button('Revert')
page.within('#modal-revert-commit') do
uncheck('create_merge_request')
click_button('Revert')
end
end end
end
context 'without creating a new merge request' do
it 'reverts a commit' do it 'reverts a commit' do
revert_commit
expect(page).to have_content('The commit has been successfully reverted.') expect(page).to have_content('The commit has been successfully reverted.')
end end
it 'does not revert a previously reverted commit' do it 'does not revert a previously reverted commit' do
revert_commit
# Visit the comment again once it was reverted. # Visit the comment again once it was reverted.
visit project_commit_path(project, sample_commit.id) visit project_commit_path(project, sample_commit.id)
find('.header-action-buttons .dropdown').click revert_commit
find('a[href="#modal-revert-commit"]').click
page.within('#modal-revert-commit') do
uncheck('create_merge_request')
click_button('Revert')
end
expect(page).to have_content('Sorry, we cannot revert this commit automatically.') expect(page).to have_content('Sorry, we cannot revert this commit automatically.')
end end
end end
context 'with creating a new merge request' do context 'with creating a new merge request' do
before do
click_revert
end
it 'reverts a commit' do it 'reverts a commit' do
page.within('#modal-revert-commit') do revert_commit(create_merge_request: true)
click_button('Revert')
end
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('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") 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 @@ ...@@ -3,6 +3,39 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe CommitsHelper do 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 describe 'commit_author_link' do
it 'escapes the author email' do it 'escapes the author email' do
commit = double( 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