Commit fa18ec94 authored by Doug Stull's avatar Doug Stull Committed by Brandon Labuschagne

Experiment: add area of focus question to invite modal

parent 5c794438
...@@ -9,13 +9,14 @@ import { ...@@ -9,13 +9,14 @@ import {
GlSprintf, GlSprintf,
GlButton, GlButton,
GlFormInput, GlFormInput,
GlFormCheckboxGroup,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { partition, isString } from 'lodash'; import { partition, isString } from 'lodash';
import Api from '~/api'; import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking'; import ExperimentTracking from '~/experimentation/experiment_tracking';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { INVITE_MEMBERS_IN_COMMENT, GROUP_FILTERS } from '../constants'; import { INVITE_MEMBERS_IN_COMMENT, GROUP_FILTERS, MEMBER_AREAS_OF_FOCUS } from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { import {
responseMessageFromError, responseMessageFromError,
...@@ -36,6 +37,7 @@ export default { ...@@ -36,6 +37,7 @@ export default {
GlSprintf, GlSprintf,
GlButton, GlButton,
GlFormInput, GlFormInput,
GlFormCheckboxGroup,
MembersTokenSelect, MembersTokenSelect,
GroupSelect, GroupSelect,
}, },
...@@ -74,6 +76,14 @@ export default { ...@@ -74,6 +76,14 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
areasOfFocusOptions: {
type: Array,
required: true,
},
noSelectionAreasOfFocus: {
type: Array,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -83,6 +93,7 @@ export default { ...@@ -83,6 +93,7 @@ export default {
inviteeType: 'members', inviteeType: 'members',
newUsersToInvite: [], newUsersToInvite: [],
selectedDate: undefined, selectedDate: undefined,
selectedAreasOfFocus: [],
groupToBeSharedWith: {}, groupToBeSharedWith: {},
source: 'unknown', source: 'unknown',
invalidFeedbackMessage: '', invalidFeedbackMessage: '',
...@@ -128,10 +139,21 @@ export default { ...@@ -128,10 +139,21 @@ export default {
this.newUsersToInvite.length === 0 && Object.keys(this.groupToBeSharedWith).length === 0 this.newUsersToInvite.length === 0 && Object.keys(this.groupToBeSharedWith).length === 0
); );
}, },
areasOfFocusEnabled() {
return this.areasOfFocusOptions.length !== 0;
},
areasOfFocusForPost() {
if (this.selectedAreasOfFocus.length === 0 && this.areasOfFocusEnabled) {
return this.noSelectionAreasOfFocus;
}
return this.selectedAreasOfFocus;
},
}, },
mounted() { mounted() {
eventHub.$on('openModal', (options) => { eventHub.$on('openModal', (options) => {
this.openModal(options); this.openModal(options);
this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.view);
}); });
}, },
methods: { methods: {
...@@ -152,9 +174,12 @@ export default { ...@@ -152,9 +174,12 @@ export default {
this.$root.$emit(BV_SHOW_MODAL, this.modalId); this.$root.$emit(BV_SHOW_MODAL, this.modalId);
}, },
trackEvent(experimentName, eventName) {
const tracking = new ExperimentTracking(experimentName);
tracking.event(eventName);
},
closeModal() { closeModal() {
this.resetFields(); this.$refs.modal.hide();
this.$root.$emit(BV_HIDE_MODAL, this.modalId);
}, },
sendInvite() { sendInvite() {
if (this.isInviteGroup) { if (this.isInviteGroup) {
...@@ -165,9 +190,10 @@ export default { ...@@ -165,9 +190,10 @@ export default {
}, },
trackInvite() { trackInvite() {
if (this.source === INVITE_MEMBERS_IN_COMMENT) { if (this.source === INVITE_MEMBERS_IN_COMMENT) {
const tracking = new ExperimentTracking(INVITE_MEMBERS_IN_COMMENT); this.trackEvent(INVITE_MEMBERS_IN_COMMENT, 'comment_invite_success');
tracking.event('comment_invite_success');
} }
this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.submit);
}, },
resetFields() { resetFields() {
this.isLoading = false; this.isLoading = false;
...@@ -176,6 +202,7 @@ export default { ...@@ -176,6 +202,7 @@ export default {
this.newUsersToInvite = []; this.newUsersToInvite = [];
this.groupToBeSharedWith = {}; this.groupToBeSharedWith = {};
this.invalidFeedbackMessage = ''; this.invalidFeedbackMessage = '';
this.selectedAreasOfFocus = [];
}, },
changeSelectedItem(item) { changeSelectedItem(item) {
this.selectedAccessLevel = item; this.selectedAccessLevel = item;
...@@ -223,6 +250,7 @@ export default { ...@@ -223,6 +250,7 @@ export default {
email: usersToInviteByEmail, email: usersToInviteByEmail,
access_level: this.selectedAccessLevel, access_level: this.selectedAccessLevel,
invite_source: this.source, invite_source: this.source,
areas_of_focus: this.areasOfFocusForPost,
}; };
}, },
addByUserIdPostData(usersToAddById) { addByUserIdPostData(usersToAddById) {
...@@ -231,6 +259,7 @@ export default { ...@@ -231,6 +259,7 @@ export default {
user_id: usersToAddById, user_id: usersToAddById,
access_level: this.selectedAccessLevel, access_level: this.selectedAccessLevel,
invite_source: this.source, invite_source: this.source,
areas_of_focus: this.areasOfFocusForPost,
}; };
}, },
shareWithGroupPostData(groupToBeSharedWith) { shareWithGroupPostData(groupToBeSharedWith) {
...@@ -304,18 +333,22 @@ export default { ...@@ -304,18 +333,22 @@ export default {
inviteButtonText: s__('InviteMembersModal|Invite'), inviteButtonText: s__('InviteMembersModal|Invite'),
cancelButtonText: s__('InviteMembersModal|Cancel'), cancelButtonText: s__('InviteMembersModal|Cancel'),
headerCloseLabel: s__('InviteMembersModal|Close invite team members'), headerCloseLabel: s__('InviteMembersModal|Close invite team members'),
areasOfFocusLabel: s__(
'InviteMembersModal|What would you like new member(s) to focus on? (optional)',
),
}, },
membersTokenSelectLabelId: 'invite-members-input', membersTokenSelectLabelId: 'invite-members-input',
}; };
</script> </script>
<template> <template>
<gl-modal <gl-modal
ref="modal"
:modal-id="modalId" :modal-id="modalId"
size="sm" size="sm"
data-qa-selector="invite_members_modal_content" data-qa-selector="invite_members_modal_content"
:title="$options.labels[inviteeType].modalTitle" :title="$options.labels[inviteeType].modalTitle"
:header-close-label="$options.labels.headerCloseLabel" :header-close-label="$options.labels.headerCloseLabel"
@close="resetFields" @hidden="resetFields"
> >
<div> <div>
<p ref="introText"> <p ref="introText">
...@@ -351,7 +384,7 @@ export default { ...@@ -351,7 +384,7 @@ export default {
/> />
</gl-form-group> </gl-form-group>
<label class="gl-font-weight-bold gl-mt-3">{{ $options.labels.accessLevel }}</label> <label class="gl-mt-3">{{ $options.labels.accessLevel }}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full"> <div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-dropdown <gl-dropdown
class="gl-shadow-none gl-w-full" class="gl-shadow-none gl-w-full"
...@@ -381,7 +414,7 @@ export default { ...@@ -381,7 +414,7 @@ export default {
</gl-sprintf> </gl-sprintf>
</div> </div>
<label class="gl-font-weight-bold gl-mt-5 gl-display-block" for="expires_at">{{ <label class="gl-mt-5 gl-display-block" for="expires_at">{{
$options.labels.accessExpireDate $options.labels.accessExpireDate
}}</label> }}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block"> <div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block">
...@@ -400,6 +433,16 @@ export default { ...@@ -400,6 +433,16 @@ export default {
</template> </template>
</gl-datepicker> </gl-datepicker>
</div> </div>
<div v-if="areasOfFocusEnabled">
<label class="gl-mt-5">
{{ $options.labels.areasOfFocusLabel }}
</label>
<gl-form-checkbox-group
v-model="selectedAreasOfFocus"
:options="areasOfFocusOptions"
data-testid="area-of-focus-checks"
/>
</div>
</div> </div>
<template #modal-footer> <template #modal-footer>
......
...@@ -3,6 +3,11 @@ import { __ } from '~/locale'; ...@@ -3,6 +3,11 @@ import { __ } from '~/locale';
export const SEARCH_DELAY = 200; export const SEARCH_DELAY = 200;
export const INVITE_MEMBERS_IN_COMMENT = 'invite_members_in_comment'; export const INVITE_MEMBERS_IN_COMMENT = 'invite_members_in_comment';
export const MEMBER_AREAS_OF_FOCUS = {
name: 'member_areas_of_focus',
view: 'view',
submit: 'submit',
};
export const GROUP_FILTERS = { export const GROUP_FILTERS = {
ALL: 'all', ALL: 'all',
......
...@@ -23,6 +23,8 @@ export default function initInviteMembersModal() { ...@@ -23,6 +23,8 @@ export default function initInviteMembersModal() {
defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10), defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10),
groupSelectFilter: el.dataset.groupsFilter, groupSelectFilter: el.dataset.groupsFilter,
groupSelectParentId: parseInt(el.dataset.parentId, 10), groupSelectParentId: parseInt(el.dataset.parentId, 10),
areasOfFocusOptions: JSON.parse(el.dataset.areasOfFocusOptions),
noSelectionAreasOfFocus: JSON.parse(el.dataset.noSelectionAreasOfFocus),
}, },
}), }),
}); });
......
...@@ -39,4 +39,43 @@ module InviteMembersHelper ...@@ -39,4 +39,43 @@ module InviteMembersHelper
{} {}
end end
end end
def common_invite_modal_dataset(source)
dataset = {
id: source.id,
name: source.name,
default_access_level: Gitlab::Access::GUEST
}
experiment(:member_areas_of_focus, user: current_user) do |e|
e.publish_to_database
e.control { dataset.merge!(areas_of_focus_options: [], no_selection_areas_of_focus: []) }
e.candidate { dataset.merge!(areas_of_focus_options: member_areas_of_focus_options.to_json, no_selection_areas_of_focus: ['no_selection']) }
end
dataset
end
private
def member_areas_of_focus_options
[
{
value: 'Contribute to the codebase', text: s_('InviteMembersModal|Contribute to the codebase')
},
{
value: 'Collaborate on open issues and merge requests', text: s_('InviteMembersModal|Collaborate on open issues and merge requests')
},
{
value: 'Configure CI/CD', text: s_('InviteMembersModal|Configure CI/CD')
},
{
value: 'Configure security features', text: s_('InviteMembersModal|Configure security features')
},
{
value: 'Other', text: s_('InviteMembersModal|Other')
}
]
end
end end
- return unless can_manage_members?(group) - return unless can_manage_members?(group)
.js-invite-members-modal{ data: { id: group.id, .js-invite-members-modal{ data: { is_project: 'false',
name: group.name,
is_project: 'false',
access_levels: GroupMember.access_level_roles.to_json, access_levels: GroupMember.access_level_roles.to_json,
default_access_level: Gitlab::Access::GUEST, help_link: help_page_url('user/permissions') }.merge(group_select_data(group)).merge(common_invite_modal_dataset(group)) }
help_link: help_page_url('user/permissions') }.merge(group_select_data(group)) }
- return unless can_import_members? - return unless can_import_members?
.js-invite-members-modal{ data: { id: project.id, .js-invite-members-modal{ data: { is_project: 'true',
name: project.name,
is_project: 'true',
access_levels: ProjectMember.access_level_roles.to_json, access_levels: ProjectMember.access_level_roles.to_json,
default_access_level: Gitlab::Access::GUEST, help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(project)) }
help_link: help_page_url('user/permissions') } }
---
name: member_areas_of_focus
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65273
rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/406
milestone: '14.2'
type: experiment
group: group::expansion
default_enabled: false
...@@ -18032,6 +18032,18 @@ msgstr "" ...@@ -18032,6 +18032,18 @@ msgstr ""
msgid "InviteMembersModal|Close invite team members" msgid "InviteMembersModal|Close invite team members"
msgstr "" msgstr ""
msgid "InviteMembersModal|Collaborate on open issues and merge requests"
msgstr ""
msgid "InviteMembersModal|Configure CI/CD"
msgstr ""
msgid "InviteMembersModal|Configure security features"
msgstr ""
msgid "InviteMembersModal|Contribute to the codebase"
msgstr ""
msgid "InviteMembersModal|GitLab member or email address" msgid "InviteMembersModal|GitLab member or email address"
msgstr "" msgstr ""
...@@ -18047,6 +18059,9 @@ msgstr "" ...@@ -18047,6 +18059,9 @@ msgstr ""
msgid "InviteMembersModal|Members were successfully added" msgid "InviteMembersModal|Members were successfully added"
msgstr "" msgstr ""
msgid "InviteMembersModal|Other"
msgstr ""
msgid "InviteMembersModal|Search for a group to invite" msgid "InviteMembersModal|Search for a group to invite"
msgstr "" msgstr ""
...@@ -18062,6 +18077,9 @@ msgstr "" ...@@ -18062,6 +18077,9 @@ msgstr ""
msgid "InviteMembersModal|Something went wrong" msgid "InviteMembersModal|Something went wrong"
msgstr "" msgstr ""
msgid "InviteMembersModal|What would you like new member(s) to focus on? (optional)"
msgstr ""
msgid "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group." msgid "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group."
msgstr "" msgstr ""
......
...@@ -84,6 +84,33 @@ RSpec.describe 'Groups > Members > Manage members' do ...@@ -84,6 +84,33 @@ RSpec.describe 'Groups > Members > Manage members' do
property: 'existing_user', property: 'existing_user',
user: user1 user: user1
) )
expect_no_snowplow_event(
category: 'Members::CreateService',
action: 'area_of_focus'
)
end
it 'adds a user to group with area_of_focus', :js, :snowplow, :aggregate_failures do
stub_experiments(member_areas_of_focus: :candidate)
group.add_owner(user1)
visit group_group_members_path(group)
invite_member(user2.name, role: 'Reporter', area_of_focus: true)
wait_for_requests
expect_snowplow_event(
category: 'Members::CreateService',
action: 'area_of_focus',
label: 'Contribute to the codebase',
property: group.members.last.id.to_s
)
expect_snowplow_event(
category: 'Members::CreateService',
action: 'area_of_focus',
label: 'Collaborate on open issues and merge requests',
property: group.members.last.id.to_s
)
end end
it 'do not disclose email addresses', :js do it 'do not disclose email addresses', :js do
...@@ -193,9 +220,36 @@ RSpec.describe 'Groups > Members > Manage members' do ...@@ -193,9 +220,36 @@ RSpec.describe 'Groups > Members > Manage members' do
property: 'net_new_user', property: 'net_new_user',
user: user1 user: user1
) )
expect_no_snowplow_event(
category: 'Members::CreateService',
action: 'area_of_focus'
)
end end
end end
it 'invite user to group with area_of_focus', :js, :snowplow, :aggregate_failures do
stub_experiments(member_areas_of_focus: :candidate)
group.add_owner(user1)
visit group_group_members_path(group)
invite_member('test@example.com', role: 'Reporter', area_of_focus: true)
wait_for_requests
expect_snowplow_event(
category: 'Members::InviteService',
action: 'area_of_focus',
label: 'Contribute to the codebase',
property: group.members.last.id.to_s
)
expect_snowplow_event(
category: 'Members::InviteService',
action: 'area_of_focus',
label: 'Collaborate on open issues and merge requests',
property: group.members.last.id.to_s
)
end
context 'when user is a guest' do context 'when user is a guest' do
before do before do
group.add_guest(user1) group.add_guest(user1)
......
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
GlSprintf, GlSprintf,
GlLink, GlLink,
GlModal, GlModal,
GlFormCheckboxGroup,
} from '@gitlab/ui'; } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
...@@ -15,7 +16,8 @@ import Api from '~/api'; ...@@ -15,7 +16,8 @@ import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking'; import ExperimentTracking from '~/experimentation/experiment_tracking';
import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue'; import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue'; import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
import { INVITE_MEMBERS_IN_COMMENT } from '~/invite_members/constants'; import { INVITE_MEMBERS_IN_COMMENT, MEMBER_AREAS_OF_FOCUS } from '~/invite_members/constants';
import eventHub from '~/invite_members/event_hub';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import { apiPaths, membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses'; import { apiPaths, membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses';
...@@ -32,7 +34,12 @@ const inviteeType = 'members'; ...@@ -32,7 +34,12 @@ const inviteeType = 'members';
const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 }; const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 };
const defaultAccessLevel = 10; const defaultAccessLevel = 10;
const inviteSource = 'unknown'; const inviteSource = 'unknown';
const noSelectionAreasOfFocus = ['no_selection'];
const helpLink = 'https://example.com'; const helpLink = 'https://example.com';
const areasOfFocusOptions = [
{ text: 'area1', value: 'area1' },
{ text: 'area2', value: 'area2' },
];
const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' }; const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' };
const user2 = { id: 2, name: 'Name Two', username: 'one_2', avatar_url: '' }; const user2 = { id: 2, name: 'Name Two', username: 'one_2', avatar_url: '' };
...@@ -58,7 +65,9 @@ const createComponent = (data = {}, props = {}) => { ...@@ -58,7 +65,9 @@ const createComponent = (data = {}, props = {}) => {
isProject, isProject,
inviteeType, inviteeType,
accessLevels, accessLevels,
areasOfFocusOptions,
defaultAccessLevel, defaultAccessLevel,
noSelectionAreasOfFocus,
helpLink, helpLink,
...props, ...props,
}, },
...@@ -119,6 +128,7 @@ describe('InviteMembersModal', () => { ...@@ -119,6 +128,7 @@ describe('InviteMembersModal', () => {
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group'); const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const membersFormGroupInvalidFeedback = () => findMembersFormGroup().props('invalidFeedback'); const membersFormGroupInvalidFeedback = () => findMembersFormGroup().props('invalidFeedback');
const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect); const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect);
const findAreaofFocusCheckBoxGroup = () => wrapper.findComponent(GlFormCheckboxGroup);
describe('rendering the modal', () => { describe('rendering the modal', () => {
beforeEach(() => { beforeEach(() => {
...@@ -164,6 +174,21 @@ describe('InviteMembersModal', () => { ...@@ -164,6 +174,21 @@ describe('InviteMembersModal', () => {
}); });
}); });
describe('rendering the areas_of_focus', () => {
it('renders the areas_of_focus checkboxes', () => {
createComponent();
expect(findAreaofFocusCheckBoxGroup().props('options')).toBe(areasOfFocusOptions);
expect(findAreaofFocusCheckBoxGroup().exists()).toBe(true);
});
it('does not render the areas_of_focus checkboxes', () => {
createComponent({}, { areasOfFocusOptions: [] });
expect(findAreaofFocusCheckBoxGroup().exists()).toBe(false);
});
});
describe('displaying the correct introText', () => { describe('displaying the correct introText', () => {
describe('when inviting to a project', () => { describe('when inviting to a project', () => {
describe('when inviting members', () => { describe('when inviting members', () => {
...@@ -214,6 +239,20 @@ describe('InviteMembersModal', () => { ...@@ -214,6 +239,20 @@ describe('InviteMembersModal', () => {
"email 'email@example.com' does not match the allowed domains: example1.org"; "email 'email@example.com' does not match the allowed domains: example1.org";
const expectedSyntaxError = 'email contains an invalid email address'; const expectedSyntaxError = 'email contains an invalid email address';
it('calls the API with the expected focus data when an areas_of_focus checkbox is clicked', () => {
const spy = jest.spyOn(Api, 'addGroupMembersByUserId');
const expectedFocus = [areasOfFocusOptions[0].value];
createComponent({ newUsersToInvite: [user1] });
findAreaofFocusCheckBoxGroup().vm.$emit('input', expectedFocus);
clickInviteButton();
expect(spy).toHaveBeenCalledWith(
user1.id.toString(),
expect.objectContaining({ areas_of_focus: expectedFocus }),
);
});
describe('when inviting an existing user to group by user ID', () => { describe('when inviting an existing user to group by user ID', () => {
const postData = { const postData = {
user_id: '1,2', user_id: '1,2',
...@@ -221,6 +260,7 @@ describe('InviteMembersModal', () => { ...@@ -221,6 +260,7 @@ describe('InviteMembersModal', () => {
expires_at: undefined, expires_at: undefined,
invite_source: inviteSource, invite_source: inviteSource,
format: 'json', format: 'json',
areas_of_focus: noSelectionAreasOfFocus,
}; };
describe('when member is added successfully', () => { describe('when member is added successfully', () => {
...@@ -230,32 +270,36 @@ describe('InviteMembersModal', () => { ...@@ -230,32 +270,36 @@ describe('InviteMembersModal', () => {
wrapper.vm.$toast = { show: jest.fn() }; wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData }); jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
jest.spyOn(wrapper.vm, 'showToastMessageSuccess'); jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
});
it('includes the non-default selected areas of focus', () => {
const focus = ['abc'];
const updatedPostData = { ...postData, areas_of_focus: focus };
wrapper.setData({ selectedAreasOfFocus: focus });
clickInviteButton(); clickInviteButton();
});
it('sets isLoading on the Invite button when it is clicked', () => { expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, updatedPostData);
expect(findInviteButton().props('loading')).toBe(true);
}); });
it('removes isLoading from the Invite button when request completes', async () => { describe('when triggered from regular mounting', () => {
await waitForPromises(); beforeEach(() => {
clickInviteButton();
expect(findInviteButton().props('loading')).toBe(false);
}); });
it('calls Api addGroupMembersByUserId with the correct params', async () => { it('sets isLoading on the Invite button when it is clicked', () => {
await waitForPromises; expect(findInviteButton().props('loading')).toBe(true);
});
it('calls Api addGroupMembersByUserId with the correct params', () => {
expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, postData); expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, postData);
}); });
it('displays the successful toastMessage', async () => { it('displays the successful toastMessage', () => {
await waitForPromises;
expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled(); expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled();
}); });
}); });
});
describe('when member is not added successfully', () => { describe('when member is not added successfully', () => {
beforeEach(() => { beforeEach(() => {
...@@ -353,6 +397,7 @@ describe('InviteMembersModal', () => { ...@@ -353,6 +397,7 @@ describe('InviteMembersModal', () => {
expires_at: undefined, expires_at: undefined,
email: 'email@example.com', email: 'email@example.com',
invite_source: inviteSource, invite_source: inviteSource,
areas_of_focus: noSelectionAreasOfFocus,
format: 'json', format: 'json',
}; };
...@@ -363,7 +408,20 @@ describe('InviteMembersModal', () => { ...@@ -363,7 +408,20 @@ describe('InviteMembersModal', () => {
wrapper.vm.$toast = { show: jest.fn() }; wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData }); jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
jest.spyOn(wrapper.vm, 'showToastMessageSuccess'); jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
});
it('includes the non-default selected areas of focus', () => {
const focus = ['abc'];
const updatedPostData = { ...postData, areas_of_focus: focus };
wrapper.setData({ selectedAreasOfFocus: focus });
clickInviteButton();
expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, updatedPostData);
});
describe('when triggered from regular mounting', () => {
beforeEach(() => {
clickInviteButton(); clickInviteButton();
}); });
...@@ -375,6 +433,7 @@ describe('InviteMembersModal', () => { ...@@ -375,6 +433,7 @@ describe('InviteMembersModal', () => {
expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled(); expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled();
}); });
}); });
});
describe('when invites are not sent successfully', () => { describe('when invites are not sent successfully', () => {
beforeEach(() => { beforeEach(() => {
...@@ -465,6 +524,7 @@ describe('InviteMembersModal', () => { ...@@ -465,6 +524,7 @@ describe('InviteMembersModal', () => {
access_level: defaultAccessLevel, access_level: defaultAccessLevel,
expires_at: undefined, expires_at: undefined,
invite_source: inviteSource, invite_source: inviteSource,
areas_of_focus: noSelectionAreasOfFocus,
format: 'json', format: 'json',
}; };
...@@ -501,7 +561,7 @@ describe('InviteMembersModal', () => { ...@@ -501,7 +561,7 @@ describe('InviteMembersModal', () => {
}); });
it('calls Apis with the invite source passed through to openModal', () => { it('calls Apis with the invite source passed through to openModal', () => {
wrapper.vm.openModal({ inviteeType: 'members', source: '_invite_source_' }); eventHub.$emit('openModal', { inviteeType: 'members', source: '_invite_source_' });
clickInviteButton(); clickInviteButton();
...@@ -579,9 +639,7 @@ describe('InviteMembersModal', () => { ...@@ -579,9 +639,7 @@ describe('InviteMembersModal', () => {
clickInviteButton(); clickInviteButton();
}); });
it('displays the generic error message', async () => { it('displays the generic error message', () => {
await waitForPromises();
expect(membersFormGroupInvalidFeedback()).toBe('Something went wrong'); expect(membersFormGroupInvalidFeedback()).toBe('Something went wrong');
}); });
}); });
...@@ -596,7 +654,7 @@ describe('InviteMembersModal', () => { ...@@ -596,7 +654,7 @@ describe('InviteMembersModal', () => {
}); });
it('tracks the invite', () => { it('tracks the invite', () => {
wrapper.vm.openModal({ inviteeType: 'members', source: INVITE_MEMBERS_IN_COMMENT }); eventHub.$emit('openModal', { inviteeType: 'members', source: INVITE_MEMBERS_IN_COMMENT });
clickInviteButton(); clickInviteButton();
...@@ -605,19 +663,37 @@ describe('InviteMembersModal', () => { ...@@ -605,19 +663,37 @@ describe('InviteMembersModal', () => {
}); });
it('does not track invite for unknown source', () => { it('does not track invite for unknown source', () => {
wrapper.vm.openModal({ inviteeType: 'members', source: 'unknown' }); eventHub.$emit('openModal', { inviteeType: 'members', source: 'unknown' });
clickInviteButton(); clickInviteButton();
expect(ExperimentTracking).not.toHaveBeenCalled(); expect(ExperimentTracking).not.toHaveBeenCalledWith(INVITE_MEMBERS_IN_COMMENT);
}); });
it('does not track invite undefined source', () => { it('does not track invite undefined source', () => {
wrapper.vm.openModal({ inviteeType: 'members' }); eventHub.$emit('openModal', { inviteeType: 'members' });
clickInviteButton(); clickInviteButton();
expect(ExperimentTracking).not.toHaveBeenCalled(); expect(ExperimentTracking).not.toHaveBeenCalledWith(INVITE_MEMBERS_IN_COMMENT);
});
it('tracks the view for areas_of_focus', () => {
eventHub.$emit('openModal', { inviteeType: 'members' });
expect(ExperimentTracking).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.name);
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.view);
});
it('tracks the invite for areas_of_focus', () => {
eventHub.$emit('openModal', { inviteeType: 'members' });
clickInviteButton();
expect(ExperimentTracking).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.name);
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
MEMBER_AREAS_OF_FOCUS.submit,
);
}); });
}); });
}); });
......
...@@ -14,6 +14,56 @@ RSpec.describe InviteMembersHelper do ...@@ -14,6 +14,56 @@ RSpec.describe InviteMembersHelper do
helper.extend(Gitlab::Experimentation::ControllerConcern) helper.extend(Gitlab::Experimentation::ControllerConcern)
end end
describe '#common_invite_modal_dataset' do
context 'when member_areas_of_focus is enabled', :experiment do
context 'with control experience' do
before do
stub_experiments(member_areas_of_focus: :control)
end
it 'has expected attributes' do
attributes = {
areas_of_focus_options: [],
no_selection_areas_of_focus: []
}
expect(helper.common_invite_modal_dataset(project)).to include(attributes)
end
end
context 'with candidate experience' do
before do
stub_experiments(member_areas_of_focus: :candidate)
end
it 'has expected attributes', :aggregate_failures do
output = helper.common_invite_modal_dataset(project)
expect(output[:no_selection_areas_of_focus]).to eq ['no_selection']
expect(Gitlab::Json.parse(output[:areas_of_focus_options]).first['value']).to eq 'Contribute to the codebase'
end
end
end
context 'when member_areas_of_focus is disabled' do
before do
stub_feature_flags(member_areas_of_focus: false)
end
it 'has expected attributes' do
attributes = {
id: project.id,
name: project.name,
default_access_level: Gitlab::Access::GUEST,
areas_of_focus_options: [],
no_selection_areas_of_focus: []
}
expect(helper.common_invite_modal_dataset(project)).to match(attributes)
end
end
end
context 'with project' do context 'with project' do
before do before do
allow(helper).to receive(:current_user) { owner } allow(helper).to receive(:current_user) { owner }
......
...@@ -5,7 +5,7 @@ module Spec ...@@ -5,7 +5,7 @@ module Spec
module Helpers module Helpers
module Features module Features
module InviteMembersModalHelper module InviteMembersModalHelper
def invite_member(name, role: 'Guest', expires_at: nil) def invite_member(name, role: 'Guest', expires_at: nil, area_of_focus: false)
click_on 'Invite members' click_on 'Invite members'
page.within '#invite-members-modal' do page.within '#invite-members-modal' do
...@@ -14,6 +14,7 @@ module Spec ...@@ -14,6 +14,7 @@ module Spec
wait_for_requests wait_for_requests
click_button name click_button name
choose_options(role, expires_at) choose_options(role, expires_at)
choose_area_of_focus if area_of_focus
click_button 'Invite' click_button 'Invite'
...@@ -41,7 +42,14 @@ module Spec ...@@ -41,7 +42,14 @@ module Spec
click_button role click_button role
end end
fill_in 'YYYY-MM-DD', with: expires_at.try(:strftime, '%Y-%m-%d') fill_in 'YYYY-MM-DD', with: expires_at.strftime('%Y-%m-%d') if expires_at
end
def choose_area_of_focus
page.within '[data-testid="area-of-focus-checks"]' do
check 'Contribute to the codebase'
check 'Collaborate on open issues and merge requests'
end
end end
end end
end end
......
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