Commit fb0a732b authored by Jackie Fraser's avatar Jackie Fraser Committed by Mark Florian

Display API errors in invite modal before closing

API errors for single invitee scenarios are
displayed in an error alert inside the modal.
Multiple failures will display the first error.
parent a673de05
<script> <script>
import { import {
GlFormGroup,
GlModal, GlModal,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
...@@ -12,16 +13,21 @@ import { ...@@ -12,16 +13,21 @@ import {
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 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 { BV_SHOW_MODAL, BV_HIDE_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 } from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import {
responseMessageFromError,
responseMessageFromSuccess,
} from '../utils/response_message_parser';
import GroupSelect from './group_select.vue';
import MembersTokenSelect from './members_token_select.vue';
export default { export default {
name: 'InviteMembersModal', name: 'InviteMembersModal',
components: { components: {
GlFormGroup,
GlDatepicker, GlDatepicker,
GlLink, GlLink,
GlModal, GlModal,
...@@ -79,9 +85,13 @@ export default { ...@@ -79,9 +85,13 @@ export default {
selectedDate: undefined, selectedDate: undefined,
groupToBeSharedWith: {}, groupToBeSharedWith: {},
source: 'unknown', source: 'unknown',
invalidFeedbackMessage: '',
}; };
}, },
computed: { computed: {
validationState() {
return this.invalidFeedbackMessage === '' ? null : false;
},
isInviteGroup() { isInviteGroup() {
return this.inviteeType === 'group'; return this.inviteeType === 'group';
}, },
...@@ -142,6 +152,7 @@ export default { ...@@ -142,6 +152,7 @@ export default {
this.$root.$emit(BV_SHOW_MODAL, this.modalId); this.$root.$emit(BV_SHOW_MODAL, this.modalId);
}, },
closeModal() { closeModal() {
this.resetFields();
this.$root.$emit(BV_HIDE_MODAL, this.modalId); this.$root.$emit(BV_HIDE_MODAL, this.modalId);
}, },
sendInvite() { sendInvite() {
...@@ -150,7 +161,6 @@ export default { ...@@ -150,7 +161,6 @@ export default {
} else { } else {
this.submitInviteMembers(); this.submitInviteMembers();
} }
this.closeModal();
}, },
trackInvite() { trackInvite() {
if (this.source === INVITE_MEMBERS_IN_COMMENT) { if (this.source === INVITE_MEMBERS_IN_COMMENT) {
...@@ -158,12 +168,12 @@ export default { ...@@ -158,12 +168,12 @@ export default {
tracking.event('comment_invite_success'); tracking.event('comment_invite_success');
} }
}, },
cancelInvite() { resetFields() {
this.selectedAccessLevel = this.defaultAccessLevel; this.selectedAccessLevel = this.defaultAccessLevel;
this.selectedDate = undefined; this.selectedDate = undefined;
this.newUsersToInvite = []; this.newUsersToInvite = [];
this.groupToBeSharedWith = {}; this.groupToBeSharedWith = {};
this.closeModal(); this.invalidFeedbackMessage = '';
}, },
changeSelectedItem(item) { changeSelectedItem(item) {
this.selectedAccessLevel = item; this.selectedAccessLevel = item;
...@@ -175,9 +185,11 @@ export default { ...@@ -175,9 +185,11 @@ export default {
apiShareWithGroup(this.id, this.shareWithGroupPostData(this.groupToBeSharedWith.id)) apiShareWithGroup(this.id, this.shareWithGroupPostData(this.groupToBeSharedWith.id))
.then(this.showToastMessageSuccess) .then(this.showToastMessageSuccess)
.catch(this.showToastMessageError); .catch(this.showInvalidFeedbackMessage);
}, },
submitInviteMembers() { submitInviteMembers() {
this.invalidFeedbackMessage = '';
const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite(); const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite();
const promises = []; const promises = [];
...@@ -196,10 +208,11 @@ export default { ...@@ -196,10 +208,11 @@ export default {
promises.push(apiAddByUserId(this.id, this.addByUserIdPostData(usersToAddById))); promises.push(apiAddByUserId(this.id, this.addByUserIdPostData(usersToAddById)));
} }
this.trackInvite(); this.trackInvite();
Promise.all(promises).then(this.showToastMessageSuccess).catch(this.showToastMessageError); Promise.all(promises)
.then(this.conditionallyShowToastSuccess)
.catch(this.showInvalidFeedbackMessage);
}, },
inviteByEmailPostData(usersToInviteByEmail) { inviteByEmailPostData(usersToInviteByEmail) {
return { return {
...@@ -224,13 +237,27 @@ export default { ...@@ -224,13 +237,27 @@ export default {
group_access: this.selectedAccessLevel, group_access: this.selectedAccessLevel,
}; };
}, },
conditionallyShowToastSuccess(response) {
const message = responseMessageFromSuccess(response);
if (message === '') {
this.showToastMessageSuccess();
return;
}
this.invalidFeedbackMessage = message;
},
showToastMessageSuccess() { showToastMessageSuccess() {
this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions); this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions);
this.closeModal();
}, },
showToastMessageError(error) { showInvalidFeedbackMessage(response) {
const message = error.response.data.message || this.$options.labels.toastMessageUnsuccessful; this.invalidFeedbackMessage =
responseMessageFromError(response) || this.$options.labels.invalidFeedbackMessageDefault;
this.$toast.show(message, this.toastOptions); },
handleMembersTokenSelectClear() {
this.invalidFeedbackMessage = '';
}, },
}, },
labels: { labels: {
...@@ -267,8 +294,8 @@ export default { ...@@ -267,8 +294,8 @@ export default {
accessLevel: s__('InviteMembersModal|Select a role'), accessLevel: s__('InviteMembersModal|Select a role'),
accessExpireDate: s__('InviteMembersModal|Access expiration date (optional)'), accessExpireDate: s__('InviteMembersModal|Access expiration date (optional)'),
toastMessageSuccessful: s__('InviteMembersModal|Members were successfully added'), toastMessageSuccessful: s__('InviteMembersModal|Members were successfully added'),
toastMessageUnsuccessful: s__('InviteMembersModal|Some of the members could not be added'), invalidFeedbackMessageDefault: s__('InviteMembersModal|Something went wrong'),
readMoreText: s__(`InviteMembersModal|%{linkStart}Learn more%{linkEnd} about roles.`), readMoreText: s__(`InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`),
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'),
...@@ -283,6 +310,7 @@ export default { ...@@ -283,6 +310,7 @@ export default {
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"
> >
<div> <div>
<p ref="introText"> <p ref="introText">
...@@ -293,15 +321,22 @@ export default { ...@@ -293,15 +321,22 @@ export default {
</gl-sprintf> </gl-sprintf>
</p> </p>
<label :id="$options.membersTokenSelectLabelId" class="gl-font-weight-bold gl-mt-5">{{ <gl-form-group
$options.labels[inviteeType].searchField class="gl-mt-2"
}}</label> :invalid-feedback="invalidFeedbackMessage"
<div class="gl-mt-2"> :state="validationState"
:description="$options.labels[inviteeType].placeHolder"
data-testid="members-form-group"
>
<label :id="$options.membersTokenSelectLabelId" class="col-form-label">{{
$options.labels[inviteeType].searchField
}}</label>
<members-token-select <members-token-select
v-if="!isInviteGroup" v-if="!isInviteGroup"
v-model="newUsersToInvite" v-model="newUsersToInvite"
:validation-state="validationState"
:aria-labelledby="$options.membersTokenSelectLabelId" :aria-labelledby="$options.membersTokenSelectLabelId"
:placeholder="$options.labels[inviteeType].placeHolder" @clear="handleMembersTokenSelectClear"
/> />
<group-select <group-select
v-if="isInviteGroup" v-if="isInviteGroup"
...@@ -309,7 +344,7 @@ export default { ...@@ -309,7 +344,7 @@ export default {
:groups-filter="groupSelectFilter" :groups-filter="groupSelectFilter"
:parent-group-id="groupSelectParentId" :parent-group-id="groupSelectParentId"
/> />
</div> </gl-form-group>
<label class="gl-font-weight-bold gl-mt-3">{{ $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"> <div class="gl-mt-2 gl-w-half gl-xs-w-full">
...@@ -364,15 +399,15 @@ export default { ...@@ -364,15 +399,15 @@ export default {
<template #modal-footer> <template #modal-footer>
<div class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0"> <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"> <gl-button data-testid="cancel-button" @click="closeModal">
{{ $options.labels.cancelButtonText }} {{ $options.labels.cancelButtonText }}
</gl-button> </gl-button>
<div class="gl-mr-3"></div> <div class="gl-mr-3"></div>
<gl-button <gl-button
ref="inviteButton"
:disabled="inviteDisabled" :disabled="inviteDisabled"
variant="success" variant="success"
data-qa-selector="invite_button" data-qa-selector="invite_button"
data-testid="invite-button"
@click="sendInvite" @click="sendInvite"
>{{ $options.labels.inviteButtonText }}</gl-button >{{ $options.labels.inviteButtonText }}</gl-button
> >
......
<script> <script>
import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlSprintf } from '@gitlab/ui'; import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlIcon, GlSprintf } from '@gitlab/ui';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { getUsers } from '~/rest_api'; import { getUsers } from '~/rest_api';
...@@ -10,6 +10,7 @@ export default { ...@@ -10,6 +10,7 @@ export default {
GlTokenSelector, GlTokenSelector,
GlAvatar, GlAvatar,
GlAvatarLabeled, GlAvatarLabeled,
GlIcon,
GlSprintf, GlSprintf,
}, },
props: { props: {
...@@ -22,6 +23,11 @@ export default { ...@@ -22,6 +23,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
validationState: {
type: Boolean,
required: false,
default: null,
},
}, },
data() { data() {
return { return {
...@@ -84,6 +90,13 @@ export default { ...@@ -84,6 +90,13 @@ export default {
this.hasBeenFocused = true; this.hasBeenFocused = true;
}, },
handleTokenRemove() {
if (this.selectedTokens.length) {
return;
}
this.$emit('clear');
},
}, },
queryOptions: { exclude_internal: true, active: true }, queryOptions: { exclude_internal: true, active: true },
i18n: { i18n: {
...@@ -95,19 +108,26 @@ export default { ...@@ -95,19 +108,26 @@ export default {
<template> <template>
<gl-token-selector <gl-token-selector
v-model="selectedTokens" v-model="selectedTokens"
:state="validationState"
:dropdown-items="users" :dropdown-items="users"
:loading="loading" :loading="loading"
:allow-user-defined-tokens="emailIsValid" :allow-user-defined-tokens="emailIsValid"
:hide-dropdown-with-no-items="hideDropdownWithNoItems" :hide-dropdown-with-no-items="hideDropdownWithNoItems"
:placeholder="placeholderText" :placeholder="placeholderText"
:aria-labelledby="ariaLabelledby" :aria-labelledby="ariaLabelledby"
:text-input-attrs="{
'data-testid': 'members-token-select-input',
'data-qa-selector': 'members_token_select_input',
}"
@blur="handleBlur" @blur="handleBlur"
@text-input="handleTextInput" @text-input="handleTextInput"
@input="handleInput" @input="handleInput"
@focus="handleFocus" @focus="handleFocus"
@token-remove="handleTokenRemove"
> >
<template #token-content="{ token }"> <template #token-content="{ token }">
<gl-avatar v-if="token.avatar_url" :src="token.avatar_url" :size="16" /> <gl-icon v-if="validationState === false" name="error" :size="16" class="gl-mr-2" />
<gl-avatar v-else-if="token.avatar_url" :src="token.avatar_url" :size="16" />
{{ token.name }} {{ token.name }}
</template> </template>
......
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';
...@@ -6,3 +8,7 @@ export const GROUP_FILTERS = { ...@@ -6,3 +8,7 @@ export const GROUP_FILTERS = {
ALL: 'all', ALL: 'all',
DESCENDANT_GROUPS: 'descendant_groups', DESCENDANT_GROUPS: 'descendant_groups',
}; };
export const API_MESSAGES = {
EMAIL_ALREADY_INVITED: __('Invite email has already been taken'),
};
import { isString } from 'lodash';
import { API_MESSAGES } from '~/invite_members/constants';
function responseKeyedMessageParsed(keyedMessage) {
try {
const keys = Object.keys(keyedMessage);
const msg = keyedMessage[keys[0]];
if (msg === API_MESSAGES.EMAIL_ALREADY_INVITED) {
return '';
}
return msg;
} catch {
return '';
}
}
function responseMessageStringForMultiple(message) {
return message.includes(':');
}
function responseMessageStringFirstPart(message) {
return message.split(' and ')[0];
}
export function responseMessageFromError(response) {
if (!response?.response?.data) {
return '';
}
const {
response: { data },
} = response;
return (
data.error ||
data.message?.user?.[0] ||
data.message?.access_level?.[0] ||
data.message?.error ||
data.message ||
''
);
}
export function responseMessageFromSuccess(response) {
if (!response?.[0]?.data) {
return '';
}
const { data } = response[0];
if (data.message && !data.message.user) {
const { message } = data;
if (isString(message)) {
if (responseMessageStringForMultiple(message)) {
return responseMessageStringFirstPart(message);
}
return message;
}
return responseKeyedMessageParsed(message);
}
return data.message || data.message?.user || data.error || '';
}
...@@ -71,14 +71,14 @@ RSpec.describe 'Groups > Members > List members' do ...@@ -71,14 +71,14 @@ RSpec.describe 'Groups > Members > List members' do
page.within '#invite-members-modal' do page.within '#invite-members-modal' do
[user1, user2].each do |user_with_saml| [user1, user2].each do |user_with_saml|
fill_in 'Select members or type email addresses', with: user_with_saml.name find('[data-testid="members-token-select-input"]').set(user_with_saml.name)
wait_for_requests wait_for_requests
expect(page).to have_content(user_with_saml.name) expect(page).to have_content(user_with_saml.name)
end end
[user3, user4].each do |user_without_saml| [user3, user4].each do |user_without_saml|
fill_in 'Select members or type email addresses', with: user_without_saml.name find('[data-testid="members-token-select-input"]').set(user_without_saml.name)
wait_for_requests wait_for_requests
expect(page).not_to have_content(user_without_saml.name) expect(page).not_to have_content(user_without_saml.name)
......
...@@ -17905,6 +17905,9 @@ msgstr "" ...@@ -17905,6 +17905,9 @@ msgstr ""
msgid "Invite a group" msgid "Invite a group"
msgstr "" msgstr ""
msgid "Invite email has already been taken"
msgstr ""
msgid "Invite group" msgid "Invite group"
msgstr "" msgstr ""
...@@ -17956,7 +17959,7 @@ msgstr "" ...@@ -17956,7 +17959,7 @@ msgstr ""
msgid "InviteMembersBanner|We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge." msgid "InviteMembersBanner|We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge."
msgstr "" msgstr ""
msgid "InviteMembersModal|%{linkStart}Learn more%{linkEnd} about roles." msgid "InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions"
msgstr "" msgstr ""
msgid "InviteMembersModal|Access expiration date (optional)" msgid "InviteMembersModal|Access expiration date (optional)"
...@@ -17995,7 +17998,7 @@ msgstr "" ...@@ -17995,7 +17998,7 @@ msgstr ""
msgid "InviteMembersModal|Select members or type email addresses" msgid "InviteMembersModal|Select members or type email addresses"
msgstr "" msgstr ""
msgid "InviteMembersModal|Some of the members could not be added" msgid "InviteMembersModal|Something went wrong"
msgstr "" 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."
......
...@@ -19,6 +19,10 @@ module QA ...@@ -19,6 +19,10 @@ module QA
element :group_select_dropdown_search_field element :group_select_dropdown_search_field
end end
base.view 'app/assets/javascripts/invite_members/components/members_token_select.vue' do
element :members_token_select_input
end
base.view 'app/assets/javascripts/invite_members/components/invite_group_trigger.vue' do base.view 'app/assets/javascripts/invite_members/components/invite_group_trigger.vue' do
element :invite_a_group_button element :invite_a_group_button
end end
...@@ -42,7 +46,7 @@ module QA ...@@ -42,7 +46,7 @@ module QA
within_element(:invite_members_modal_content) do within_element(:invite_members_modal_content) do
fill_element :access_level_dropdown, with: access_level fill_element :access_level_dropdown, with: access_level
fill_in 'Select members or type email addresses', with: username fill_element :members_token_select_input, username
Support::WaitForRequests.wait_for_requests Support::WaitForRequests.wait_for_requests
......
...@@ -5,7 +5,7 @@ module QA ...@@ -5,7 +5,7 @@ module QA
describe 'Email Notification' do describe 'Email Notification' do
include Support::Api include Support::Api
let(:user) do let!(:user) do
Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
end end
......
...@@ -93,13 +93,13 @@ RSpec.describe 'Groups > Members > Manage members' do ...@@ -93,13 +93,13 @@ RSpec.describe 'Groups > Members > Manage members' do
visit group_group_members_path(group) visit group_group_members_path(group)
click_on 'Invite members' click_on 'Invite members'
fill_in 'Select members or type email addresses', with: '@gitlab.com' find('[data-testid="members-token-select-input"]').set('@gitlab.com')
wait_for_requests wait_for_requests
expect(page).to have_content('No matches found') expect(page).to have_content('No matches found')
fill_in 'Select members or type email addresses', with: 'undisclosed_email@gitlab.com' find('[data-testid="members-token-select-input"]').set('undisclosed_email@gitlab.com')
wait_for_requests wait_for_requests
expect(page).to have_content("Jane 'invisible' Doe") expect(page).to have_content("Jane 'invisible' Doe")
......
...@@ -115,6 +115,21 @@ describe('MembersTokenSelect', () => { ...@@ -115,6 +115,21 @@ describe('MembersTokenSelect', () => {
expect(wrapper.emitted().input[0][0]).toEqual([user1, user2]); expect(wrapper.emitted().input[0][0]).toEqual([user1, user2]);
}); });
}); });
describe('when user is removed', () => {
it('emits `clear` event', () => {
findTokenSelector().vm.$emit('token-remove', [user1]);
expect(wrapper.emitted('clear')).toEqual([[]]);
});
it('does not emit `clear` event when there are still tokens selected', () => {
findTokenSelector().vm.$emit('input', [user1, user2]);
findTokenSelector().vm.$emit('token-remove', [user1]);
expect(wrapper.emitted('clear')).toBeUndefined();
});
});
}); });
describe('when text input is blurred', () => { describe('when text input is blurred', () => {
......
const INVITATIONS_API_EMAIL_INVALID = {
message: { error: 'email contains an invalid email address' },
};
const INVITATIONS_API_ERROR_EMAIL_INVALID = {
error: 'email contains an invalid email address',
};
const INVITATIONS_API_EMAIL_RESTRICTED = {
message: {
'email@example.com':
"Invite email 'email@example.com' does not match the allowed domains: example1.org",
},
status: 'error',
};
const INVITATIONS_API_MULTIPLE_EMAIL_RESTRICTED = {
message: {
'email@example.com':
"Invite email email 'email@example.com' does not match the allowed domains: example1.org",
'email4@example.com':
"Invite email email 'email4@example.com' does not match the allowed domains: example1.org",
},
status: 'error',
};
const INVITATIONS_API_EMAIL_TAKEN = {
message: {
'email@example2.com': 'Invite email has already been taken',
},
status: 'error',
};
const MEMBERS_API_MEMBER_ALREADY_EXISTS = {
message: 'Member already exists',
};
const MEMBERS_API_SINGLE_USER_RESTRICTED = {
message: { user: ["email 'email@example.com' does not match the allowed domains: example1.org"] },
};
const MEMBERS_API_SINGLE_USER_ACCESS_LEVEL = {
message: {
access_level: [
'should be greater than or equal to Owner inherited membership from group Gitlab Org',
],
},
};
const MEMBERS_API_MULTIPLE_USERS_RESTRICTED = {
message:
"root: User email 'admin@example.com' does not match the allowed domain of example2.com and user18: User email 'user18@example.org' does not match the allowed domain of example2.com",
status: 'error',
};
export const apiPaths = {
GROUPS_MEMBERS: '/api/v4/groups/1/members',
GROUPS_INVITATIONS: '/api/v4/groups/1/invitations',
};
export const membersApiResponse = {
MEMBER_ALREADY_EXISTS: MEMBERS_API_MEMBER_ALREADY_EXISTS,
SINGLE_USER_ACCESS_LEVEL: MEMBERS_API_SINGLE_USER_ACCESS_LEVEL,
SINGLE_USER_RESTRICTED: MEMBERS_API_SINGLE_USER_RESTRICTED,
MULTIPLE_USERS_RESTRICTED: MEMBERS_API_MULTIPLE_USERS_RESTRICTED,
};
export const invitationsApiResponse = {
EMAIL_INVALID: INVITATIONS_API_EMAIL_INVALID,
ERROR_EMAIL_INVALID: INVITATIONS_API_ERROR_EMAIL_INVALID,
EMAIL_RESTRICTED: INVITATIONS_API_EMAIL_RESTRICTED,
MULTIPLE_EMAIL_RESTRICTED: INVITATIONS_API_MULTIPLE_EMAIL_RESTRICTED,
EMAIL_TAKEN: INVITATIONS_API_EMAIL_TAKEN,
};
import {
responseMessageFromSuccess,
responseMessageFromError,
} from '~/invite_members/utils/response_message_parser';
describe('Response message parser', () => {
const expectedMessage = 'expected display message';
describe('parse message from successful response', () => {
const exampleKeyedMsg = { 'email@example.com': expectedMessage };
const exampleUserMsgMultiple =
' and username1: id not found and username2: email is restricted';
it.each([
[[{ data: { message: expectedMessage } }]],
[[{ data: { message: expectedMessage + exampleUserMsgMultiple } }]],
[[{ data: { error: expectedMessage } }]],
[[{ data: { message: [expectedMessage] } }]],
[[{ data: { message: exampleKeyedMsg } }]],
])(`returns "${expectedMessage}" from success response: %j`, (successResponse) => {
expect(responseMessageFromSuccess(successResponse)).toBe(expectedMessage);
});
});
describe('message from error response', () => {
it.each([
[{ response: { data: { error: expectedMessage } } }],
[{ response: { data: { message: { user: [expectedMessage] } } } }],
[{ response: { data: { message: { access_level: [expectedMessage] } } } }],
[{ response: { data: { message: { error: expectedMessage } } } }],
[{ response: { data: { message: expectedMessage } } }],
])(`returns "${expectedMessage}" from error response: %j`, (errorResponse) => {
expect(responseMessageFromError(errorResponse)).toBe(expectedMessage);
});
});
});
...@@ -9,7 +9,7 @@ module Spec ...@@ -9,7 +9,7 @@ module Spec
click_on 'Invite members' click_on 'Invite members'
page.within '#invite-members-modal' do page.within '#invite-members-modal' do
fill_in 'Select members or type email addresses', with: name find('[data-testid="members-token-select-input"]').set(name)
wait_for_requests wait_for_requests
click_button name click_button name
......
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