Commit 718218e8 authored by Jackie Fraser's avatar Jackie Fraser Committed by Scott Hampton

Add "Invite a group" feature to invite members modal [RUN ALL RSPEC]

parent 7c6ca329
......@@ -24,6 +24,7 @@ const Api = {
projectPackagesPath: '/api/:version/projects/:id/packages',
projectPackagePath: '/api/:version/projects/:id/packages/:package_id',
groupProjectsPath: '/api/:version/groups/:id/projects.json',
groupSharePath: '/api/:version/groups/:id/share',
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
forkedProjectsPath: '/api/:version/projects/:id/forks',
......@@ -39,6 +40,7 @@ const Api = {
projectRunnersPath: '/api/:version/projects/:id/runners',
projectProtectedBranchesPath: '/api/:version/projects/:id/protected_branches',
projectSearchPath: '/api/:version/projects/:id/search',
projectSharePath: '/api/:version/projects/:id/share',
projectMilestonesPath: '/api/:version/projects/:id/milestones',
projectIssuePath: '/api/:version/projects/:id/issues/:issue_iid',
mergeRequestsPath: '/api/:version/merge_requests',
......@@ -365,6 +367,16 @@ const Api = {
});
},
projectShareWithGroup(id, options = {}) {
const url = Api.buildUrl(Api.projectSharePath).replace(':id', encodeURIComponent(id));
return axios.post(url, {
expires_at: options.expires_at,
group_access: options.group_access,
group_id: options.group_id,
});
},
projectMilestones(id, params = {}) {
const url = Api.buildUrl(Api.projectMilestonesPath).replace(':id', encodeURIComponent(id));
......@@ -426,6 +438,16 @@ const Api = {
});
},
groupShareWithGroup(id, options = {}) {
const url = Api.buildUrl(Api.groupSharePath).replace(':id', encodeURIComponent(id));
return axios.post(url, {
expires_at: options.expires_at,
group_access: options.group_access,
group_id: options.group_id,
});
},
commit(id, sha, params = {}) {
const url = Api.buildUrl(this.commitPath)
.replace(':id', encodeURIComponent(id))
......
<script>
import { GlDropdown, GlDropdownItem, GlDropdownText, GlSearchBoxByType } from '@gitlab/ui';
import { debounce } from 'lodash';
import Api from '~/api';
import { s__ } from '~/locale';
import { SEARCH_DELAY } from '../constants';
export default {
name: 'GroupSelect',
components: {
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlSearchBoxByType,
},
model: {
prop: 'selectedGroup',
},
data() {
return {
isFetching: false,
groups: [],
selectedGroup: {},
searchTerm: '',
};
},
computed: {
selectedGroupName() {
return this.selectedGroup.name || this.$options.i18n.dropdownText;
},
isFetchResultEmpty() {
return this.groups.length === 0;
},
},
watch: {
searchTerm() {
this.retrieveGroups();
},
},
mounted() {
this.retrieveGroups();
},
methods: {
retrieveGroups: debounce(function debouncedRetrieveGroups() {
this.isFetching = true;
return Api.groups(this.searchTerm, this.$options.defaultFetchOptions)
.then((response) => {
this.groups = response.map((group) => ({
id: group.id,
name: group.full_name,
path: group.path,
}));
this.isFetching = false;
})
.catch(() => {
this.isFetching = false;
});
}, SEARCH_DELAY),
selectGroup(group) {
this.selectedGroup = group;
this.$emit('input', this.selectedGroup);
},
},
i18n: {
dropdownText: s__('GroupSelect|Select a group'),
searchPlaceholder: s__('GroupSelect|Search groups'),
emptySearchResult: s__('GroupSelect|No matching results'),
},
defaultFetchOptions: {
exclude_internal: true,
active: true,
},
};
</script>
<template>
<div>
<gl-dropdown
data-testid="group-select-dropdown"
:text="selectedGroupName"
block
menu-class="gl-w-full!"
>
<gl-search-box-by-type
v-model.trim="searchTerm"
:is-loading="isFetching"
:placeholder="$options.i18n.searchPlaceholder"
data-qa-selector="group_select_dropdown_search_field"
/>
<gl-dropdown-item
v-for="group in groups"
:key="group.id"
:name="group.name"
@click="selectGroup(group)"
>
{{ group.name }}
</gl-dropdown-item>
<gl-dropdown-text v-if="isFetchResultEmpty && !isFetching" data-testid="empty-result-message">
<span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span>
</gl-dropdown-text>
</gl-dropdown>
</div>
</template>
<script>
import { GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
GlButton,
},
props: {
displayText: {
type: String,
required: false,
default: s__('InviteMembers|Invite a group'),
},
classes: {
type: String,
required: false,
default: '',
},
},
methods: {
openModal() {
eventHub.$emit('openModal', { inviteeType: 'group' });
},
},
};
</script>
<template>
<gl-button :class="classes" data-qa-selector="invite_a_group_button" @click="openModal">
{{ displayText }}
</gl-button>
</template>
......@@ -11,9 +11,10 @@ import {
} from '@gitlab/ui';
import { partition, isString } from 'lodash';
import Api from '~/api';
import GroupSelect from '~/invite_members/components/group_select.vue';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { s__, __, sprintf } from '~/locale';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
......@@ -28,6 +29,7 @@ export default {
GlButton,
GlFormInput,
MembersTokenSelect,
GroupSelect,
},
props: {
id: {
......@@ -60,21 +62,21 @@ export default {
visible: true,
modalId: 'invite-members-modal',
selectedAccessLevel: this.defaultAccessLevel,
inviteeType: 'members',
newUsersToInvite: [],
selectedDate: undefined,
groupToBeSharedWith: {},
};
},
computed: {
inviteToName() {
return this.name.toUpperCase();
},
inviteToType() {
return this.isProject ? __('project') : __('group');
isInviteGroup() {
return this.inviteeType === 'group';
},
introText() {
return sprintf(s__("InviteMembersModal|You're inviting members to the %{name} %{type}"), {
name: this.inviteToName,
type: this.inviteToType,
const inviteTo = this.isProject ? 'toProject' : 'toGroup';
return sprintf(this.$options.labels[this.inviteeType][inviteTo].introText, {
name: this.name.toUpperCase(),
});
},
toastOptions() {
......@@ -82,12 +84,12 @@ export default {
onComplete: () => {
this.selectedAccessLevel = this.defaultAccessLevel;
this.newUsersToInvite = [];
this.groupToBeSharedWith = {};
},
};
},
basePostData() {
return {
access_level: this.selectedAccessLevel,
expires_at: this.selectedDate,
format: 'json',
};
......@@ -97,9 +99,16 @@ export default {
(key) => this.accessLevels[key] === Number(this.selectedAccessLevel),
);
},
inviteDisabled() {
return (
this.newUsersToInvite.length === 0 && Object.keys(this.groupToBeSharedWith).length === 0
);
},
},
mounted() {
eventHub.$on('openModal', this.openModal);
eventHub.$on('openModal', (options) => {
this.openModal(options);
});
},
methods: {
partitionNewUsersToInvite() {
......@@ -113,26 +122,42 @@ export default {
usersToAddById.map((user) => user.id).join(','),
];
},
openModal() {
openModal({ inviteeType }) {
this.inviteeType = inviteeType;
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
closeModal() {
this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
sendInvite() {
this.submitForm();
if (this.isInviteGroup) {
this.submitShareWithGroup();
} else {
this.submitInviteMembers();
}
this.closeModal();
},
cancelInvite() {
this.selectedAccessLevel = this.defaultAccessLevel;
this.selectedDate = undefined;
this.newUsersToInvite = '';
this.newUsersToInvite = [];
this.groupToBeSharedWith = {};
this.closeModal();
},
changeSelectedItem(item) {
this.selectedAccessLevel = item;
},
submitForm() {
submitShareWithGroup() {
const apiShareWithGroup = this.isProject
? Api.projectShareWithGroup.bind(Api)
: Api.groupShareWithGroup.bind(Api);
apiShareWithGroup(this.id, this.shareWithGroupPostData(this.groupToBeSharedWith.id))
.then(this.showToastMessageSuccess)
.catch(this.showToastMessageError);
},
submitInviteMembers() {
const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite();
const promises = [];
......@@ -155,10 +180,25 @@ export default {
Promise.all(promises).then(this.showToastMessageSuccess).catch(this.showToastMessageError);
},
inviteByEmailPostData(usersToInviteByEmail) {
return { ...this.basePostData, email: usersToInviteByEmail };
return {
...this.basePostData,
email: usersToInviteByEmail,
access_level: this.selectedAccessLevel,
};
},
addByUserIdPostData(usersToAddById) {
return { ...this.basePostData, user_id: usersToAddById };
return {
...this.basePostData,
user_id: usersToAddById,
access_level: this.selectedAccessLevel,
};
},
shareWithGroupPostData(groupToBeSharedWith) {
return {
...this.basePostData,
group_id: groupToBeSharedWith,
group_access: this.selectedAccessLevel,
};
},
showToastMessageSuccess() {
this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions);
......@@ -170,9 +210,28 @@ export default {
},
},
labels: {
modalTitle: s__('InviteMembersModal|Invite team members'),
newUsersToInvite: s__('InviteMembersModal|GitLab member or Email address'),
userPlaceholder: s__('InviteMembersModal|Search for members to invite'),
members: {
modalTitle: s__('InviteMembersModal|Invite team members'),
searchField: s__('InviteMembersModal|GitLab member or Email address'),
placeHolder: s__('InviteMembersModal|Search for members to invite'),
toGroup: {
introText: s__("InviteMembersModal|You're inviting members to the %{name} group"),
},
toProject: {
introText: s__("InviteMembersModal|You're inviting members to the %{name} project"),
},
},
group: {
modalTitle: s__('InviteMembersModal|Invite a group'),
searchField: s__('InviteMembersModal|Select a group to invite'),
placeHolder: s__('InviteMembersModal|Search for a group to invite'),
toGroup: {
introText: s__("InviteMembersModal|You're inviting a group to the %{name} group"),
},
toProject: {
introText: s__("InviteMembersModal|You're inviting a group to the %{name} project"),
},
},
accessLevel: s__('InviteMembersModal|Choose a role permission'),
accessExpireDate: s__('InviteMembersModal|Access expiration date (optional)'),
toastMessageSuccessful: s__('InviteMembersModal|Members were successfully added'),
......@@ -189,27 +248,34 @@ export default {
<gl-modal
:modal-id="modalId"
size="sm"
:title="$options.labels.modalTitle"
data-qa-selector="invite_members_modal_content"
:title="$options.labels[inviteeType].modalTitle"
:header-close-label="$options.labels.headerCloseLabel"
>
<div class="gl-ml-5 gl-mr-5">
<div>{{ introText }}</div>
<div>
<p ref="introText">{{ introText }}</p>
<label :id="$options.membersTokenSelectLabelId" class="gl-font-weight-bold gl-mt-5">{{
$options.labels.newUsersToInvite
$options.labels[inviteeType].searchField
}}</label>
<div class="gl-mt-2">
<members-token-select
v-if="!isInviteGroup"
v-model="newUsersToInvite"
:label="$options.labels.newUsersToInvite"
:aria-labelledby="$options.membersTokenSelectLabelId"
:placeholder="$options.labels.userPlaceholder"
:placeholder="$options.labels[inviteeType].placeHolder"
/>
<group-select v-if="isInviteGroup" v-model="groupToBeSharedWith" />
</div>
<label class="gl-font-weight-bold gl-mt-5">{{ $options.labels.accessLevel }}</label>
<label class="gl-font-weight-bold gl-mt-3">{{ $options.labels.accessLevel }}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-dropdown class="gl-shadow-none gl-w-full" v-bind="$attrs" :text="selectedRoleName">
<gl-dropdown
class="gl-shadow-none gl-w-full"
data-qa-selector="access_level_dropdown"
v-bind="$attrs"
:text="selectedRoleName"
>
<template v-for="(key, item) in accessLevels">
<gl-dropdown-item
:key="key"
......@@ -223,7 +289,7 @@ export default {
</gl-dropdown>
</div>
<div class="gl-mt-2">
<div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-sprintf :message="$options.labels.readMoreText">
<template #link="{ content }">
<gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
......@@ -231,7 +297,7 @@ export default {
</gl-sprintf>
</div>
<label class="gl-font-weight-bold gl-mt-5" for="expires_at">{{
<label class="gl-font-weight-bold gl-mt-5 gl-display-block" for="expires_at">{{
$options.labels.accessExpireDate
}}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block">
......@@ -253,15 +319,16 @@ export default {
</div>
<template #modal-footer>
<div class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-p-3">
<div class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0">
<gl-button ref="cancelButton" @click="cancelInvite">
{{ $options.labels.cancelButtonText }}
</gl-button>
<div class="gl-mr-3"></div>
<gl-button
ref="inviteButton"
:disabled="!newUsersToInvite"
:disabled="inviteDisabled"
variant="success"
data-qa-selector="invite_button"
@click="sendInvite"
>{{ $options.labels.inviteButtonText }}</gl-button
>
......
......@@ -27,14 +27,14 @@ export default {
},
methods: {
openModal() {
eventHub.$emit('openModal');
eventHub.$emit('openModal', { inviteeType: 'members' });
},
},
};
</script>
<template>
<gl-link :class="classes" @click="openModal">
<gl-link :class="classes" data-qa-selector="invite_members_button" @click="openModal">
<div v-if="icon" class="nav-icon-container">
<gl-icon :size="16" :name="icon" />
</div>
......
......@@ -3,7 +3,7 @@ import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlSprintf } from '@gitlab/u
import { debounce } from 'lodash';
import { __ } from '~/locale';
import { getUsers } from '~/rest_api';
import { USER_SEARCH_DELAY } from '../constants';
import { SEARCH_DELAY } from '../constants';
export default {
components: {
......@@ -67,7 +67,7 @@ export default {
.catch(() => {
this.loading = false;
});
}, USER_SEARCH_DELAY),
}, SEARCH_DELAY),
handleInput() {
this.$emit('input', this.selectedTokens);
},
......
export const USER_SEARCH_DELAY = 200;
export const SEARCH_DELAY = 200;
import Vue from 'vue';
import InviteGroupTrigger from '~/invite_members/components/invite_group_trigger.vue';
export default function initInviteGroupTrigger() {
const el = document.querySelector('.js-invite-group-trigger');
if (!el) {
return false;
}
return new Vue({
el,
render: (createElement) =>
createElement(InviteGroupTrigger, {
props: {
...el.dataset,
},
}),
});
}
import Vue from 'vue';
import { groupMemberRequestFormatter } from '~/groups/members/utils';
import groupsSelect from '~/groups_select';
import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { s__ } from '~/locale';
......@@ -70,5 +71,6 @@ memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal();
initInviteMembersModal();
initInviteMembersTrigger();
initInviteGroupTrigger();
new UsersSelect(); // eslint-disable-line no-new
import Vue from 'vue';
import { deprecatedCreateFlash as flash } from '~/flash';
import groupsSelect from '~/groups_select';
import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { __ } from '~/locale';
......@@ -29,6 +30,7 @@ memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal();
initInviteMembersModal();
initInviteMembersTrigger();
initInviteGroupTrigger();
new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
......
......@@ -16,8 +16,9 @@
= html_escape(_('You can invite a new member to %{strong_start}%{group_name}%{strong_end}.')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- if can_invite_members_for_group?(@group)
.gl-w-half.gl-xs-w-full
.gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2.gl-mb-3
.js-invite-members-trigger.gl-px-2.gl-sm-w-auto.gl-w-full.gl-mb-4{ data: { classes: 'btn btn-success gl-button gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite members') } }
.gl-display-flex.gl-flex-wrap.gl-justify-content-end.gl-mb-3
.js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite a group') } }
.js-invite-members-trigger{ data: { classes: 'btn btn-success gl-button gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
= render 'groups/invite_members_modal', group: @group
- if can_manage_members && !can_invite_members_for_group?(@group)
%hr.gl-mt-4
......
......@@ -18,8 +18,9 @@
%p
= html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: '<i>'.html_safe, i_close: '</i>'.html_safe }
.col-md-12.col-lg-6
.gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2.gl-mb-3
.js-invite-members-trigger.gl-px-2.gl-sm-w-auto.gl-w-full.gl-mb-4{ data: { classes: 'btn btn-success gl-button gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite members') } }
.gl-display-flex.gl-flex-wrap.gl-justify-content-end
.js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite a group') } }
.js-invite-members-trigger{ data: { classes: 'btn btn-success gl-button gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
= render 'projects/invite_members_modal', project: @project
- else
......
......@@ -72,7 +72,9 @@ RSpec.describe 'Project > Members > Invite group and members', :js do
it_behaves_like 'the project can be shared with groups and members'
context 'when `vue_project_members_list` feature flag is enabled' do
it 'allows the project to be shared with another group' do
it 'allows the project to be shared with another group using the invite form' do
stub_feature_flags(invite_members_group_modal: false)
visit project_project_members_path(project)
click_on 'invite-group-tab'
......@@ -87,6 +89,26 @@ RSpec.describe 'Project > Members > Invite group and members', :js do
expect(page).to have_content(group_to_share_with.name)
end
end
it 'allows the project to be shared with another group using the invite modal' do
stub_feature_flags(invite_members_group_modal: true)
visit project_project_members_path(project)
click_on 'Invite a group'
click_on 'Select a group'
wait_for_requests
click_button group_to_share_with.name
click_button 'Invite'
visit project_project_members_path(project)
click_link 'Groups'
page.within(members_table) do
expect(page).to have_content(group_to_share_with.name)
end
end
end
context 'when `vue_project_members_list` feature flag is disabled' do
......
......@@ -14476,6 +14476,15 @@ msgstr ""
msgid "GroupSAML|should be a random persistent ID, emails are discouraged"
msgstr ""
msgid "GroupSelect|No matching results"
msgstr ""
msgid "GroupSelect|Search groups"
msgstr ""
msgid "GroupSelect|Select a group"
msgstr ""
msgid "GroupSettings|Apply integration settings to all Projects"
msgstr ""
......@@ -16300,6 +16309,9 @@ msgstr ""
msgid "Invite Members"
msgstr ""
msgid "Invite a group"
msgstr ""
msgid "Invite group"
msgstr ""
......@@ -16396,19 +16408,40 @@ msgstr ""
msgid "InviteMembersModal|Invite"
msgstr ""
msgid "InviteMembersModal|Invite a group"
msgstr ""
msgid "InviteMembersModal|Invite team members"
msgstr ""
msgid "InviteMembersModal|Members were successfully added"
msgstr ""
msgid "InviteMembersModal|Search for a group to invite"
msgstr ""
msgid "InviteMembersModal|Search for members to invite"
msgstr ""
msgid "InviteMembersModal|Select a group to invite"
msgstr ""
msgid "InviteMembersModal|Some of the members could not be added"
msgstr ""
msgid "InviteMembersModal|You're inviting members to the %{name} %{type}"
msgid "InviteMembersModal|You're inviting a group to the %{name} group"
msgstr ""
msgid "InviteMembersModal|You're inviting a group to the %{name} project"
msgstr ""
msgid "InviteMembersModal|You're inviting members to the %{name} group"
msgstr ""
msgid "InviteMembersModal|You're inviting members to the %{name} project"
msgstr ""
msgid "InviteMembers|Invite a group"
msgstr ""
msgid "InviteMembers|Invite team members"
......
......@@ -489,6 +489,7 @@ module QA
autoload :ProjectSelector, 'qa/page/component/project_selector'
autoload :Snippet, 'qa/page/component/snippet'
autoload :NewSnippet, 'qa/page/component/new_snippet'
autoload :InviteMembersModal, 'qa/page/component/invite_members_modal'
module Issuable
autoload :Common, 'qa/page/component/issuable/common'
......
......@@ -5,6 +5,16 @@ module QA
module Project
module_function
def add_member(project:, username:)
project.visit!
Page::Project::Menu.perform(&:click_members)
Page::Project::Members.perform do |member_settings|
member_settings.add_member(username)
end
end
def go_to_create_project_from_template
if Page::Project::NewExperiment.perform(&:shown?)
Page::Project::NewExperiment.perform(&:click_create_from_template_link)
......
# frozen_string_literal: true
module QA
module Page
module Component
module InviteMembersModal
extend QA::Page::PageConcern
def self.included(base)
super
base.view 'app/assets/javascripts/invite_members/components/invite_members_modal.vue' do
element :invite_button
element :access_level_dropdown
element :invite_members_modal_content
end
base.view 'app/assets/javascripts/invite_members/components/group_select.vue' do
element :group_select_dropdown_search_field
end
base.view 'app/assets/javascripts/invite_members/components/invite_group_trigger.vue' do
element :invite_a_group_button
end
base.view 'app/assets/javascripts/invite_members/components/invite_members_trigger.vue' do
element :invite_members_button
end
end
def open_invite_members_modal
click_element :invite_members_button
end
def open_invite_group_modal
click_element :invite_a_group_button
end
def add_member(username, access_level = Resource::Members::AccessLevel::DEVELOPER)
open_invite_members_modal
within_element(:invite_members_modal_content) do
fill_element :access_level_dropdown, with: access_level
fill_in 'Search for members to invite', with: username
Support::WaitForRequests.wait_for_requests
click_button username
click_element :invite_button
end
Support::WaitForRequests.wait_for_requests
page.refresh
end
def invite_group(group_name, group_access = Resource::Members::AccessLevel::GUEST)
open_invite_group_modal
fill_element :access_level_dropdown, with: group_access
click_button 'Select a group'
fill_element :group_select_dropdown_search_field, group_name
Support::WaitForRequests.wait_for_requests
click_button group_name
click_element :invite_button
Support::WaitForRequests.wait_for_requests
page.refresh
end
end
end
end
end
......@@ -4,18 +4,13 @@ module QA
module Page
module Group
class Members < Page::Base
include QA::Page::Component::Select2
include Page::Component::InviteMembersModal
include Page::Component::UsersSelect
view 'app/assets/javascripts/vue_shared/components/remove_member_modal.vue' do
element :remove_member_modal_content
end
view 'app/views/shared/members/_invite_member.html.haml' do
element :member_select_field
element :invite_member_button
end
view 'app/assets/javascripts/pages/groups/group_members/index.js' do
element :member_row
element :groups_list
......@@ -32,31 +27,9 @@ module QA
end
view 'app/views/groups/group_members/index.html.haml' do
element :invite_group_tab
element :groups_list_tab
end
view 'app/views/shared/members/_invite_group.html.haml' do
element :group_select_field
element :invite_group_button
end
def select_group(group_name)
click_element :group_select_field
search_and_select(group_name)
end
def invite_group(group_name)
click_element :invite_group_tab
select_group(group_name)
click_element :invite_group_button
end
def add_member(username)
select_user :member_select_field, username
click_element :invite_member_button
end
def update_access_level(username, access_level)
within_element(:member_row, text: username) do
click_element :access_level_dropdown
......
......@@ -4,21 +4,18 @@ module QA
module Page
module Project
class Members < Page::Base
include QA::Page::Component::Select2
view 'app/views/shared/members/_invite_member.html.haml' do
element :member_select_field
element :invite_member_button
end
include QA::Page::Component::InviteMembersModal
view 'app/views/projects/project_members/index.html.haml' do
element :invite_group_tab
element :groups_list_tab
end
view 'app/views/shared/members/_invite_group.html.haml' do
element :group_select_field
element :invite_group_button
view 'app/assets/javascripts/invite_members/components/invite_group_trigger.vue' do
element :invite_a_group_button
end
view 'app/assets/javascripts/invite_members/components/invite_members_trigger.vue' do
element :invite_members_button
end
view 'app/assets/javascripts/pages/projects/project_members/index.js' do
......@@ -33,25 +30,7 @@ module QA
element :remove_group_link_modal_content
end
def select_group(group_name)
click_element :group_select_field
search_and_select(group_name)
end
def invite_group(group_name)
click_element :invite_group_tab
select_group(group_name)
click_element :invite_group_button
end
def add_member(username)
click_element :member_select_field
search_and_select username
click_element :invite_member_button
end
def remove_group(group_name)
click_element :invite_group_tab
click_element :groups_list_tab
within_element(:group_row, text: group_name) do
......
......@@ -26,10 +26,23 @@ module QA
JSON.parse(get(Runtime::API::Request.new(api_client, api_members_path).url).body)
end
def invite_group(group, access_level = AccessLevel::GUEST)
Support::Retrier.retry_until do
QA::Runtime::Logger.debug(%Q[Sharing #{self.class.name} with #{group.name}])
response = post Runtime::API::Request.new(api_client, api_share_path).url, { group_id: group.id, group_access: access_level }
response.code == QA::Support::Api::HTTP_STATUS_CREATED
end
end
def api_members_path
"#{api_get_path}/members"
end
def api_share_path
"#{api_get_path}/share"
end
class AccessLevel
NO_ACCESS = 0
MINIMAL_ACCESS = 5
......
......@@ -22,7 +22,7 @@ module QA
token_page.fill_token_name(name || 'api-test-token')
token_page.check_api
# Expire in 2 days just in case the token is created just before midnight
token_page.fill_expiry_date(Date.today + 2)
token_page.fill_expiry_date(Time.now.utc.to_date + 2)
token_page.click_create_token_button
end
end
......
......@@ -272,10 +272,6 @@ module QA
parse_body(get(Runtime::API::Request.new(api_client, api_pipelines_path).url))
end
def share_with_group(invitee, access_level = Resource::Members::AccessLevel::DEVELOPER)
post Runtime::API::Request.new(api_client, "/projects/#{id}/share").url, { group_id: invitee.id, group_access: access_level }
end
private
def transform_api_resource(api_resource)
......
......@@ -29,6 +29,7 @@ module QA
end
before do
Runtime::Feature.enable(:invite_members_group_modal, group: group)
group.add_member(developer_user, Resource::Members::AccessLevel::DEVELOPER)
end
......
......@@ -31,6 +31,7 @@ module QA
let(:two_fa_expected_text) { /The group settings for.*require you to enable Two-Factor Authentication for your account.*You need to do this before/ }
before do
Runtime::Feature.enable(:invite_members_group_modal, group: group)
group.add_member(developer_user, Resource::Members::AccessLevel::DEVELOPER)
end
......
......@@ -6,7 +6,6 @@ module QA
before do
Runtime::Feature.enable('vue_project_members_list')
end
after do
Runtime::Feature.disable('vue_project_members_list')
end
......@@ -16,9 +15,13 @@ module QA
user = Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
Resource::Project.fabricate_via_api! do |project|
project = Resource::Project.fabricate_via_api! do |project|
project.name = 'add-member-project'
end.visit!
end
Runtime::Feature.enable(:invite_members_group_modal)
project.visit!
Page::Project::Menu.perform(&:click_members)
Page::Project::Members.perform do |members|
......
......@@ -21,6 +21,7 @@ module QA
end
before do
Runtime::Feature.enable(:invite_members_group_modal, group: group)
group.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
end
......
......@@ -10,6 +10,10 @@ module QA
end
end
before do
Runtime::Feature.enable(:invite_members_group_modal, project: project)
end
let(:developer_user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
let(:maintainer_user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2) }
let(:tag_name) { 'v0.0.1' }
......
......@@ -17,6 +17,7 @@ module QA
before do
Runtime::Feature.enable('vue_project_members_list', project: project)
Runtime::Feature.enable(:invite_members_group_modal)
Flow::Login.sign_in
end
......
......@@ -18,6 +18,8 @@ module QA
describe 'check xss occurence in @mentions in issues', :requires_admin do
before do
Runtime::Feature.enable(:invite_members_group_modal)
Flow::Login.sign_in
project.add_member(user)
......
......@@ -13,6 +13,7 @@ module QA
before do
Flow::Login.sign_in
Runtime::Feature.enable(:invite_members_group_modal, project: project)
project.add_member(user)
......
......@@ -14,6 +14,7 @@ module QA
before do
Runtime::Feature.enable('real_time_issue_sidebar', project: project)
Runtime::Feature.enable('broadcast_issue_updates', project: project)
Runtime::Feature.enable(:invite_members_group_modal, project: project)
Flow::Login.sign_in
......
......@@ -19,6 +19,7 @@ module QA
end
before do
Runtime::Feature.enable(:invite_members_group_modal, project: parent_project)
parent_project.add_member(user)
end
......
......@@ -24,23 +24,24 @@ module QA
end
end
describe 'Group' do
describe 'Group', :requires_admin do
let(:group) do
Resource::Group.fabricate_via_api! do |resource|
resource.path = "test-group-#{SecureRandom.hex(8)}"
end
end
before do
@event_count = get_audit_event_count(group)
end
let(:project) do
Resource::Project.fabricate_via_api! do |resource|
resource.name = 'project-shared-with-group'
end
end
before do
@event_count = get_audit_event_count(group)
Runtime::Feature.enable(:invite_members_group_modal)
end
let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
context 'Add group', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/733' do
......@@ -103,7 +104,7 @@ module QA
context 'Add and remove project access', :requires_admin, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/735' do
before do
Runtime::Feature.enable('vue_project_members_list', project: project)
Runtime::Feature.enable(:invite_members_group_modal)
sign_in
project.visit!
......
......@@ -12,14 +12,21 @@ module QA
end
before(:all) do
@original_personal_access_token = Runtime::Env.personal_access_token
# Todo: Remove the 5 lines below when invite_members_group_modal feature flag is enabled by default or removed
# We need to nil out any existing personal token generated for the non-admin LDAP user and also set Runtime::Env.ldap_username=nil so that a new admin token is created for use to enable the feature flag.
Runtime::Env.personal_access_token = nil
ldap_username = Runtime::Env.ldap_username
Runtime::Env.ldap_username = nil
Runtime::Feature.enable(:invite_members_group_modal)
Runtime::Env.ldap_username = ldap_username
# Create the sandbox group as the LDAP user. Without this the admin user
# would own the sandbox group and then in subsequent tests the LDAP user
# would not have enough permission to push etc.
Resource::Sandbox.fabricate_via_api!
# Create an admin personal access token and use it for the remaining API calls
@original_personal_access_token = Runtime::Env.personal_access_token
Page::Main::Menu.perform do |menu|
menu.sign_out if menu.has_personal_area?
end
......
......@@ -13,6 +13,8 @@ module QA
sandbox_group.path = "saml_sso_group_#{SecureRandom.hex(8)}"
end
Runtime::Feature.enable(:invite_members_group_modal, group: @group)
@developer_user = Resource::User.fabricate_via_api!
@group.add_member(@developer_user)
......
......@@ -15,6 +15,8 @@ module QA
sandbox_group.path = "saml_sso_group_#{SecureRandom.hex(8)}"
end
Runtime::Feature.enable(:invite_members_group_modal, group: @group)
@saml_idp_service = Flow::Saml.run_saml_idp_service(@group.path)
@api_client = Runtime::API::Client.new(:gitlab, personal_access_token: Runtime::Env.admin_personal_access_token)
......
......@@ -24,6 +24,8 @@ module QA
project.initialize_with_readme = true
end
Runtime::Feature.enable(:invite_members_group_modal, project: @project)
@project.add_member(@user)
@api_client = Runtime::API::Client.new(:gitlab, user: @user)
......
......@@ -31,6 +31,9 @@ module QA
end
before do
Runtime::Feature.enable(:invite_members_group_modal, group: source_group_with_members)
Runtime::Feature.enable(:invite_members_group_modal, group: target_group_with_project)
source_group_with_members.add_member(maintainer_user, Resource::Members::AccessLevel::MAINTAINER)
end
......
......@@ -11,7 +11,7 @@ module QA
end
end
describe 'Project' do
describe 'Project', :requires_admin do
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'awesome-project'
......@@ -38,6 +38,7 @@ module QA
context "Add user access as guest", testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/729' do
before do
Runtime::Feature.enable(:invite_members_group_modal)
project.visit!
Page::Project::Menu.perform(&:click_members)
......
......@@ -12,6 +12,8 @@ module QA
end
before do
Runtime::Feature.enable(:invite_members_group_modal, project: label_board_list.project)
Flow::Login.sign_in
label_board_list.project.add_member(qa_user, Resource::Members::AccessLevel::GUEST)
......
......@@ -15,6 +15,8 @@ module QA
project.name = 'project-to-test-issue-with-multiple-assignees'
end
Runtime::Feature.enable(:invite_members_group_modal, project: project)
project.add_member(user_1)
project.add_member(user_2)
project.add_member(user_3)
......
......@@ -10,6 +10,8 @@ module QA
end
before do
Runtime::Feature.enable(:invite_members_group_modal, project: project)
Flow::Login.sign_in
user_1 = Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
......
......@@ -15,6 +15,8 @@ module QA
end
before do
Runtime::Feature.enable(:invite_members_group_modal, project: project)
Runtime::Feature.enable(:invite_members_group_modal, group: project.group)
project.add_member(approver1)
project.group.add_member(approver2)
......
......@@ -17,6 +17,7 @@ module QA
let(:branch_name) { 'protected-branch' }
before do
Runtime::Feature.enable(:invite_members_group_modal, project: project)
project.add_member(approver, Resource::Members::AccessLevel::DEVELOPER)
project.add_member(non_approver, Resource::Members::AccessLevel::DEVELOPER)
......
# frozen_string_literal: true
module QA
RSpec.describe 'Create' do
RSpec.describe 'Create', :requires_admin do
describe 'Codeowners' do
let(:files) do
[
......@@ -26,6 +26,7 @@ module QA
@project = Resource::Project.fabricate_via_api! do |project|
project.name = "codeowners"
end
Runtime::Feature.enable(:invite_members_group_modal)
@project.visit!
Page::Project::Menu.perform(&:click_members)
......
......@@ -20,6 +20,9 @@ module QA
end
before do
Runtime::Feature.enable(:invite_members_group_modal, project: project)
Runtime::Feature.enable(:invite_members_group_modal, group: root_group)
group_or_project.add_member(approver, Resource::Members::AccessLevel::MAINTAINER)
Flow::Login.sign_in
......
......@@ -18,6 +18,8 @@ module QA
end
before do
Runtime::Feature.enable(:invite_members_group_modal)
group_or_project.add_member(approver, Resource::Members::AccessLevel::MAINTAINER)
Flow::Login.sign_in
......
......@@ -213,6 +213,8 @@ module QA
project.name = 'push_rules'
end
Runtime::Feature.enable(:invite_members_group_modal, project: @project)
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = @project
push.files = standard_file
......
......@@ -80,7 +80,7 @@ module QA
login
group.add_member(user_developer, Resource::Members::AccessLevel::DEVELOPER)
project.share_with_group(group, Resource::Members::AccessLevel::DEVELOPER)
project.invite_group(group, Resource::Members::AccessLevel::DEVELOPER)
project.add_member(user_maintainer, Resource::Members::AccessLevel::MAINTAINER)
......
......@@ -69,6 +69,7 @@ module QA
end
before do
Runtime::Feature.enable(:invite_members_group_modal, project: project)
Flow::Login.sign_in
project.visit!
Flow::MergeRequest.enable_merge_trains
......
......@@ -15,7 +15,7 @@ RSpec.describe 'Groups > Members > Manage members' do
sign_in(user1)
end
shared_examples 'includes the correct Invite Members link' do |should_include, should_not_include|
shared_examples 'includes the correct Invite link' do |should_include, should_not_include|
it 'includes either the form or the modal trigger' do
group.add_owner(user1)
......@@ -31,15 +31,13 @@ RSpec.describe 'Groups > Members > Manage members' do
stub_feature_flags(invite_members_group_modal: true)
end
it_behaves_like 'includes the correct Invite Members link', '.js-invite-members-trigger', '.invite-users-form'
it_behaves_like 'includes the correct Invite link', '.js-invite-members-trigger', '.invite-users-form'
it_behaves_like 'includes the correct Invite link', '.js-invite-group-trigger', '.invite-group-form'
end
context 'when Invite Members modal is disabled' do
before do
stub_feature_flags(invite_members_group_modal: false)
end
it_behaves_like 'includes the correct Invite Members link', '.invite-users-form', '.js-invite-members-trigger'
it_behaves_like 'includes the correct Invite link', '.invite-users-form', '.js-invite-members-trigger'
it_behaves_like 'includes the correct Invite link', '.invite-group-form', '.js-invite-group-trigger'
end
it 'update user to owner level', :js do
......
......@@ -482,6 +482,30 @@ describe('Api', () => {
});
});
describe('projectShareWithGroup', () => {
it('invites a group to share access with the authenticated project', () => {
const projectId = 1;
const sharedGroupId = 99;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/share`;
const options = {
group_id: sharedGroupId,
group_access: 10,
expires_at: undefined,
};
jest.spyOn(axios, 'post');
mock.onPost(expectedUrl).reply(200, {
status: 'success',
});
return Api.projectShareWithGroup(projectId, options).then(({ data }) => {
expect(data.status).toBe('success');
expect(axios.post).toHaveBeenCalledWith(expectedUrl, options);
});
});
});
describe('projectMilestones', () => {
it('fetches project milestones', (done) => {
const projectId = 1;
......@@ -638,6 +662,30 @@ describe('Api', () => {
});
});
describe('groupShareWithGroup', () => {
it('invites a group to share access with the authenticated group', () => {
const groupId = 1;
const sharedGroupId = 99;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/share`;
const options = {
group_id: sharedGroupId,
group_access: 10,
expires_at: undefined,
};
jest.spyOn(axios, 'post');
mock.onPost(expectedUrl).reply(200, {
status: 'success',
});
return Api.groupShareWithGroup(groupId, options).then(({ data }) => {
expect(data.status).toBe('success');
expect(axios.post).toHaveBeenCalledWith(expectedUrl, options);
});
});
});
describe('commit', () => {
const projectId = 'user/project';
const sha = 'abcd0123';
......
import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import GroupSelect from '~/invite_members/components/group_select.vue';
const createComponent = () => {
return mount(GroupSelect, {});
};
const group1 = { id: 1, full_name: 'Group One' };
const group2 = { id: 2, full_name: 'Group Two' };
const allGroups = [group1, group2];
describe('GroupSelect', () => {
let wrapper;
beforeEach(() => {
jest.spyOn(Api, 'groups').mockResolvedValue(allGroups);
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownToggle = () => findDropdown().find('button[aria-haspopup="true"]');
const findDropdownItemByText = (text) =>
wrapper
.findAllComponents(GlDropdownItem)
.wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.text() === text);
it('renders GlSearchBoxByType with default attributes', () => {
expect(findSearchBoxByType().exists()).toBe(true);
expect(findSearchBoxByType().vm.$attrs).toMatchObject({
placeholder: 'Search groups',
});
});
describe('when user types in the search input', () => {
let resolveApiRequest;
beforeEach(() => {
jest.spyOn(Api, 'groups').mockImplementation(
() =>
new Promise((resolve) => {
resolveApiRequest = resolve;
}),
);
findSearchBoxByType().vm.$emit('input', group1.name);
});
it('calls the API', () => {
resolveApiRequest({ data: allGroups });
expect(Api.groups).toHaveBeenCalledWith(group1.name, {
active: true,
exclude_internal: true,
});
});
it('displays loading icon while waiting for API call to resolve', async () => {
expect(findSearchBoxByType().props('isLoading')).toBe(true);
resolveApiRequest({ data: allGroups });
await waitForPromises();
expect(findSearchBoxByType().props('isLoading')).toBe(false);
});
});
describe('when group is selected from the dropdown', () => {
beforeEach(() => {
findDropdownItemByText(group1.full_name).vm.$emit('click');
});
it('emits `input` event used by `v-model`', () => {
expect(wrapper.emitted('input')[0][0].id).toEqual(group1.id);
});
it('sets dropdown toggle text to selected item', () => {
expect(findDropdownToggle().text()).toBe(group1.full_name);
});
});
});
import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import InviteGroupTrigger from '~/invite_members/components/invite_group_trigger.vue';
import eventHub from '~/invite_members/event_hub';
const displayText = 'Invite a group';
const createComponent = (props = {}) => {
return mount(InviteGroupTrigger, {
propsData: {
displayText,
...props,
},
});
};
describe('InviteGroupTrigger', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findButton = () => wrapper.findComponent(GlButton);
describe('displayText', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('includes the correct displayText for the link', () => {
expect(findButton().text()).toBe(displayText);
});
});
describe('when button is clicked', () => {
beforeEach(() => {
eventHub.$emit = jest.fn();
wrapper = createComponent();
findButton().trigger('click');
});
it('emits event that triggers opening the modal', () => {
expect(eventHub.$emit).toHaveBeenLastCalledWith('openModal', { inviteeType: 'group' });
});
});
});
......@@ -6,8 +6,9 @@ import Api from '~/api';
import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue';
const id = '1';
const name = 'testgroup';
const name = 'test name';
const isProject = false;
const inviteeType = 'members';
const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 };
const defaultAccessLevel = '10';
const helpLink = 'https://example.com';
......@@ -20,16 +21,19 @@ const user3 = {
username: 'one_2',
avatar_url: '',
};
const sharedGroup = { id: '981' };
const createComponent = (data = {}) => {
const createComponent = (data = {}, props = {}) => {
return shallowMount(InviteMembersModal, {
propsData: {
id,
name,
isProject,
inviteeType,
accessLevels,
defaultAccessLevel,
helpLink,
...props,
},
data() {
return data;
......@@ -46,6 +50,22 @@ const createComponent = (data = {}) => {
});
};
const createInviteMembersToProjectWrapper = () => {
return createComponent({ inviteeType: 'members' }, { isProject: true });
};
const createInviteMembersToGroupWrapper = () => {
return createComponent({ inviteeType: 'members' }, { isProject: false });
};
const createInviteGroupToProjectWrapper = () => {
return createComponent({ inviteeType: 'group' }, { isProject: true });
};
const createInviteGroupToGroupWrapper = () => {
return createComponent({ inviteeType: 'group' }, { isProject: false });
};
describe('InviteMembersModal', () => {
let wrapper;
......@@ -54,12 +74,13 @@ describe('InviteMembersModal', () => {
wrapper = null;
});
const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownItems = () => findDropdown().findAll(GlDropdownItem);
const findDatepicker = () => wrapper.find(GlDatepicker);
const findLink = () => wrapper.find(GlLink);
const findCancelButton = () => wrapper.find({ ref: 'cancelButton' });
const findInviteButton = () => wrapper.find({ ref: 'inviteButton' });
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => findDropdown().findAllComponents(GlDropdownItem);
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
const findLink = () => wrapper.findComponent(GlLink);
const findIntroText = () => wrapper.find({ ref: 'introText' }).text();
const findCancelButton = () => wrapper.findComponent({ ref: 'cancelButton' });
const findInviteButton = () => wrapper.findComponent({ ref: 'inviteButton' });
const clickInviteButton = () => findInviteButton().vm.$emit('click');
describe('rendering the modal', () => {
......@@ -68,7 +89,7 @@ describe('InviteMembersModal', () => {
});
it('renders the modal with the correct title', () => {
expect(wrapper.find(GlModal).props('title')).toBe('Invite team members');
expect(wrapper.findComponent(GlModal).props('title')).toBe('Invite team members');
});
it('renders the Cancel button text correctly', () => {
......@@ -102,6 +123,44 @@ describe('InviteMembersModal', () => {
});
});
describe('displaying the correct introText', () => {
describe('when inviting to a project', () => {
describe('when inviting members', () => {
it('includes the correct invitee, type, and formatted name', () => {
wrapper = createInviteMembersToProjectWrapper();
expect(findIntroText()).toBe("You're inviting members to the TEST NAME project");
});
});
describe('when sharing with a group', () => {
it('includes the correct invitee, type, and formatted name', () => {
wrapper = createInviteGroupToProjectWrapper();
expect(findIntroText()).toBe("You're inviting a group to the TEST NAME project");
});
});
});
describe('when inviting to a group', () => {
describe('when inviting members', () => {
it('includes the correct invitee, type, and formatted name', () => {
wrapper = createInviteMembersToGroupWrapper();
expect(wrapper.html()).toContain("You're inviting members to the TEST NAME group");
});
});
describe('when sharing with a group', () => {
it('includes the correct invitee, type, and formatted name', () => {
wrapper = createInviteGroupToGroupWrapper();
expect(wrapper.html()).toContain("You're inviting a group to the TEST NAME group");
});
});
});
});
describe('submitting the invite form', () => {
const apiErrorMessage = 'Member already exists';
......@@ -115,8 +174,9 @@ describe('InviteMembersModal', () => {
describe('when invites are sent successfully', () => {
beforeEach(() => {
wrapper = createComponent({ newUsersToInvite: [user1] });
wrapper = createInviteMembersToGroupWrapper();
wrapper.setData({ newUsersToInvite: [user1] });
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
......@@ -283,5 +343,58 @@ describe('InviteMembersModal', () => {
});
});
});
describe('when inviting a group to share', () => {
describe('when sharing the group is successful', () => {
const groupPostData = {
group_id: sharedGroup.id,
group_access: '10',
expires_at: undefined,
format: 'json',
};
beforeEach(() => {
wrapper = createComponent({ groupToBeSharedWith: sharedGroup });
wrapper.setData({ inviteeType: 'group' });
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'groupShareWithGroup').mockResolvedValue({ data: groupPostData });
jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
clickInviteButton();
});
it('calls Api groupShareWithGroup with the correct params', () => {
expect(Api.groupShareWithGroup).toHaveBeenCalledWith(id, groupPostData);
});
it('displays the successful toastMessage', () => {
expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled();
});
});
describe('when sharing the group fails', () => {
beforeEach(() => {
wrapper = createComponent({ groupToBeSharedWith: sharedGroup });
wrapper.setData({ inviteeType: 'group' });
wrapper.vm.$toast = { show: jest.fn() };
jest
.spyOn(Api, 'groupShareWithGroup')
.mockRejectedValue({ response: { data: { success: false } } });
jest.spyOn(wrapper.vm, 'showToastMessageError');
clickInviteButton();
});
it('displays the generic error toastMessage', async () => {
await waitForPromises();
expect(wrapper.vm.showToastMessageError).toHaveBeenCalled();
});
});
});
});
});
......@@ -23,7 +23,7 @@ describe('InviteMembersTrigger', () => {
});
describe('displayText', () => {
const findLink = () => wrapper.find(GlLink);
const findLink = () => wrapper.findComponent(GlLink);
beforeEach(() => {
wrapper = createComponent();
......@@ -35,7 +35,7 @@ describe('InviteMembersTrigger', () => {
});
describe('icon', () => {
const findIcon = () => wrapper.find(GlIcon);
const findIcon = () => wrapper.findComponent(GlIcon);
it('includes the correct icon when an icon is sent', () => {
wrapper = createComponent({ icon });
......
......@@ -37,7 +37,7 @@ describe('MembersTokenSelect', () => {
wrapper = null;
});
const findTokenSelector = () => wrapper.find(GlTokenSelector);
const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
describe('rendering the token-selector component', () => {
it('renders with the correct props', () => {
......
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