import { GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import { stubComponent } from 'helpers/stub_component'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import Api from '~/api'; import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue'; import InviteModalBase from '~/invite_members/components/invite_modal_base.vue'; import ModalConfetti from '~/invite_members/components/confetti.vue'; import MembersTokenSelect from '~/invite_members/components/members_token_select.vue'; import UserLimitNotification from '~/invite_members/components/user_limit_notification.vue'; import { MEMBERS_MODAL_CELEBRATE_INTRO, MEMBERS_MODAL_CELEBRATE_TITLE, MEMBERS_PLACEHOLDER, MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT, EXPANDED_ERRORS, EMPTY_INVITES_ALERT_TEXT, ON_CELEBRATION_TRACK_LABEL, INVITE_MEMBER_MODAL_TRACKING_CATEGORY, INVALID_FEEDBACK_MESSAGE_DEFAULT, } from '~/invite_members/constants'; import eventHub from '~/invite_members/event_hub'; import ContentTransition from '~/vue_shared/components/content_transition.vue'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_CREATED, HTTP_STATUS_INTERNAL_SERVER_ERROR, } from '~/lib/utils/http_status'; import { displaySuccessfulInvitationAlert, reloadOnInvitationSuccess, } from '~/invite_members/utils/trigger_successful_invite_alert'; import { GROUPS_INVITATIONS_PATH, invitationsApiResponse } from '../mock_data/api_responses'; import { propsData, emailPostData, postData, singleUserPostData, newProjectPath, user1, user2, user3, user4, user5, user6, GlEmoji, } from '../mock_data/member_modal'; jest.mock('~/invite_members/utils/trigger_successful_invite_alert'); jest.mock('~/experimentation/experiment_tracking'); describe('InviteMembersModal', () => { let wrapper; let mock; let trackingSpy; const showToast = jest.fn(); const expectTracking = (action, label = undefined, property = undefined) => expect(trackingSpy).toHaveBeenCalledWith(INVITE_MEMBER_MODAL_TRACKING_CATEGORY, action, { label, category: INVITE_MEMBER_MODAL_TRACKING_CATEGORY, property, }); const createComponent = (props = {}, stubs = {}) => { wrapper = shallowMountExtended(InviteMembersModal, { provide: { newProjectPath, name: propsData.name, }, propsData: { usersLimitDataset: {}, activeTrialDataset: {}, fullPath: 'project', ...propsData, ...props, }, stubs: { InviteModalBase, ContentTransition, GlSprintf, GlModal: stubComponent(GlModal, { template: '
', }), GlEmoji, ...stubs, }, mocks: { $toast: { show: showToast, }, }, }); }; const createInviteMembersToProjectWrapper = ( usersLimitDataset = {}, activeTrialDataset = {}, stubs = {}, ) => { createComponent({ usersLimitDataset, activeTrialDataset, isProject: true }, stubs); }; const createInviteMembersToGroupWrapper = ( usersLimitDataset = {}, activeTrialDataset = {}, stubs = {}, ) => { createComponent({ usersLimitDataset, activeTrialDataset, isProject: false }, stubs); }; beforeEach(() => { gon.api_version = 'v4'; mock = new MockAdapter(axios); }); afterEach(() => { mock.restore(); }); const findModal = () => wrapper.findComponent(GlModal); const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text(); const findEmptyInvitesAlert = () => wrapper.findByTestId('empty-invites-alert'); const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error'); const findMoreInviteErrorsButton = () => wrapper.findByTestId('accordion-button'); const findUserLimitAlert = () => wrapper.findComponent(UserLimitNotification); const findAccordion = () => wrapper.findComponent(GlCollapse); const findErrorsIcon = () => wrapper.findComponent(GlIcon); const findMemberErrorMessage = (element) => `${Object.keys(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element]}: ${ Object.values(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element] }`; const findActionButton = () => wrapper.findByTestId('invite-modal-submit'); const findCancelButton = () => wrapper.findByTestId('invite-modal-cancel'); const emitClickFromModal = (findButton) => () => findButton().vm.$emit('click', { preventDefault: jest.fn() }); const clickInviteButton = emitClickFromModal(findActionButton); const clickCancelButton = emitClickFromModal(findCancelButton); const findMembersFormGroup = () => wrapper.findByTestId('members-form-group'); const membersFormGroupInvalidFeedback = () => findMembersFormGroup().attributes('invalid-feedback'); const membersFormGroupDescription = () => findMembersFormGroup().attributes('description'); const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect); const findCelebrationEmoji = () => wrapper.findComponent(GlEmoji); const triggerOpenModal = async ({ mode = 'default', source } = {}) => { eventHub.$emit('openModal', { mode, source }); await nextTick(); }; const triggerMembersTokenSelect = async (val) => { findMembersSelect().vm.$emit('input', val); await nextTick(); }; const removeMembersToken = async (val) => { findMembersSelect().vm.$emit('token-remove', val); await nextTick(); }; describe('rendering with tracking considerations', () => { describe('when inviting to a project', () => { describe('when inviting members', () => { beforeEach(() => { createInviteMembersToProjectWrapper(); }); it('renders the modal without confetti', () => { expect(wrapper.findComponent(ModalConfetti).exists()).toBe(false); }); it('includes the correct invitee', () => { expect(findIntroText()).toBe("You're inviting members to the test name project."); expect(findCelebrationEmoji().exists()).toBe(false); }); describe('members form group description', () => { it('renders correct description', () => { createInviteMembersToProjectWrapper({ GlFormGroup }); expect(membersFormGroupDescription()).toContain(MEMBERS_PLACEHOLDER); }); }); }); describe('when inviting members with celebration', () => { beforeEach(async () => { createInviteMembersToProjectWrapper(); await triggerOpenModal({ mode: 'celebrate', source: ON_CELEBRATION_TRACK_LABEL }); }); it('renders the modal with confetti', () => { expect(wrapper.findComponent(ModalConfetti).exists()).toBe(true); }); it('renders the modal with the correct title', () => { expect(findModal().props('title')).toBe(MEMBERS_MODAL_CELEBRATE_TITLE); }); it('includes the correct celebration text and emoji', () => { expect(findIntroText()).toBe( `${MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT} ${MEMBERS_MODAL_CELEBRATE_INTRO}`, ); expect(findCelebrationEmoji().exists()).toBe(true); }); describe('members form group description', () => { it('renders correct description', async () => { createInviteMembersToProjectWrapper({ GlFormGroup }); await triggerOpenModal({ mode: 'celebrate' }); expect(membersFormGroupDescription()).toContain(MEMBERS_PLACEHOLDER); }); }); describe('tracking', () => { it('tracks actions', async () => { trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); await triggerOpenModal({ mode: 'celebrate', source: ON_CELEBRATION_TRACK_LABEL }); expectTracking('render', ON_CELEBRATION_TRACK_LABEL); clickCancelButton(); expectTracking('click_cancel', ON_CELEBRATION_TRACK_LABEL); findModal().vm.$emit('close'); expectTracking('click_x', ON_CELEBRATION_TRACK_LABEL); unmockTracking(); }); }); }); }); describe('when inviting to a group', () => { it('includes the correct invitee, type, and formatted name', () => { createInviteMembersToGroupWrapper(); expect(findIntroText()).toBe("You're inviting members to the test name group."); }); describe('members form group description', () => { it('renders correct description', () => { createInviteMembersToGroupWrapper({ GlFormGroup }); expect(membersFormGroupDescription()).toContain(MEMBERS_PLACEHOLDER); }); }); }); describe('tracking', () => { it.each` desc | source | label ${'unknown'} | ${{}} | ${'unknown'} ${'known'} | ${{ source: '_invite_source_' }} | ${'_invite_source_'} `('tracks actions with $desc source', async ({ source, label }) => { createInviteMembersToProjectWrapper(); trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); await triggerOpenModal(source); expectTracking('render', label); clickCancelButton(); expectTracking('click_cancel', label); findModal().vm.$emit('close'); expectTracking('click_x', label); unmockTracking(); }); }); }); describe('rendering the user limit notification', () => { it('shows the user limit notification alert when reached limit', () => { const usersLimitDataset = { alertVariant: 'reached' }; createInviteMembersToProjectWrapper(usersLimitDataset); expect(findUserLimitAlert().exists()).toBe(true); }); it('shows the user limit notification alert when close to dashboard limit', () => { const usersLimitDataset = { alertVariant: 'close' }; createInviteMembersToProjectWrapper(usersLimitDataset); expect(findUserLimitAlert().exists()).toBe(true); }); it('shows the user limit notification alert when :preview_free_user_cap is enabled', () => { const usersLimitDataset = { alertVariant: 'notification' }; createInviteMembersToProjectWrapper(usersLimitDataset); expect(findUserLimitAlert().exists()).toBe(true); }); it('does not show the user limit notification alert', () => { const usersLimitDataset = {}; createInviteMembersToProjectWrapper(usersLimitDataset); expect(findUserLimitAlert().exists()).toBe(false); }); }); describe('submitting the invite form', () => { const mockInvitationsApi = (code, data) => { mock.onPost(GROUPS_INVITATIONS_PATH).reply(code, data); }; const expectedEmailRestrictedError = "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups."; const expectedSyntaxError = 'email contains an invalid email address'; describe('when no invites have been entered in the form and then some are entered', () => { beforeEach(() => { createInviteMembersToGroupWrapper(); }); it('displays an error', async () => { clickInviteButton(); await waitForPromises(); expect(findEmptyInvitesAlert().text()).toBe(EMPTY_INVITES_ALERT_TEXT); expect(membersFormGroupInvalidFeedback()).toBe(MEMBERS_PLACEHOLDER); expect(findMembersSelect().props('exceptionState')).toBe(false); await triggerMembersTokenSelect([user1]); expect(membersFormGroupInvalidFeedback()).toBe(''); }); }); describe('when inviting an existing user to group by user ID', () => { describe('when reloadOnSubmit is true', () => { beforeEach(async () => { createComponent({ reloadPageOnSubmit: true }); await triggerMembersTokenSelect([user1, user2]); jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData }); clickInviteButton(); }); it('calls displaySuccessfulInvitationAlert on mount', () => { expect(displaySuccessfulInvitationAlert).toHaveBeenCalled(); }); it('calls reloadOnInvitationSuccess', () => { expect(reloadOnInvitationSuccess).toHaveBeenCalled(); }); it('does not show the toast message', () => { expect(showToast).not.toHaveBeenCalled(); }); }); describe('when member is added successfully', () => { beforeEach(async () => { createComponent(); await triggerMembersTokenSelect([user1, user2]); jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData }); }); describe('when triggered from regular mounting', () => { beforeEach(() => { clickInviteButton(); }); it('calls Api inviteGroupMembers with the correct params', () => { expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData); }); it('displays the successful toastMessage', () => { expect(showToast).toHaveBeenCalledWith('Members were successfully added'); }); it('does not call displaySuccessfulInvitationAlert on mount', () => { expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled(); }); it('does not call reloadOnInvitationSuccess', () => { expect(reloadOnInvitationSuccess).not.toHaveBeenCalled(); }); }); }); describe('when member is not added successfully', () => { beforeEach(async () => { createInviteMembersToGroupWrapper(); await triggerMembersTokenSelect([user1]); }); describe('clearing the invalid state and message', () => { beforeEach(async () => { mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_TAKEN); clickInviteButton(); await waitForPromises(); }); it('clears the error when the list of members to invite is cleared', async () => { expect(findMemberErrorAlert().exists()).toBe(true); expect(findMemberErrorAlert().text()).toContain( Object.values(invitationsApiResponse.EMAIL_TAKEN.message)[0], ); expect(membersFormGroupInvalidFeedback()).toBe(''); expect(findMembersSelect().props('exceptionState')).not.toBe(false); findMembersSelect().vm.$emit('clear'); await nextTick(); expect(findMemberErrorAlert().exists()).toBe(false); expect(membersFormGroupInvalidFeedback()).toBe(''); expect(findMembersSelect().props('exceptionState')).not.toBe(false); }); it('clears the error when the cancel button is clicked', async () => { clickCancelButton(); await nextTick(); expect(membersFormGroupInvalidFeedback()).toBe(''); expect(findMembersSelect().props('exceptionState')).not.toBe(false); }); it('clears the error when the modal is hidden', async () => { findModal().vm.$emit('hidden'); await nextTick(); expect(findMemberErrorAlert().exists()).toBe(false); expect(membersFormGroupInvalidFeedback()).toBe(''); expect(findMembersSelect().props('exceptionState')).not.toBe(false); }); }); it('displays the generic error for http server error', async () => { mockInvitationsApi( HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Request failed with status code 500', ); clickInviteButton(); await waitForPromises(); expect(membersFormGroupInvalidFeedback()).toBe('Something went wrong'); expect(findMembersSelect().props('exceptionState')).toBe(false); }); it('displays the restricted user api message for response with bad request', async () => { mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_RESTRICTED); clickInviteButton(); await waitForPromises(); expect(findMemberErrorAlert().exists()).toBe(true); expect(findMemberErrorAlert().text()).toContain(expectedEmailRestrictedError); expect(membersFormGroupInvalidFeedback()).toBe(''); expect(findMembersSelect().props('exceptionState')).not.toBe(false); }); it('displays all errors when there are multiple existing users that are restricted by email', async () => { mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED); clickInviteButton(); await waitForPromises(); expect(findMemberErrorAlert().exists()).toBe(true); expect(findMemberErrorAlert().text()).toContain( Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[0], ); expect(findMemberErrorAlert().text()).toContain( Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[1], ); expect(findMemberErrorAlert().text()).toContain( Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[2], ); expect(membersFormGroupInvalidFeedback()).toBe(''); expect(findMembersSelect().props('exceptionState')).not.toBe(false); }); it('displays invite limit error message', async () => { mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.INVITE_LIMIT); clickInviteButton(); await waitForPromises(); expect(membersFormGroupInvalidFeedback()).toBe( invitationsApiResponse.INVITE_LIMIT.message, ); }); }); }); describe('when inviting a new user by email address', () => { describe('when invites are sent successfully', () => { beforeEach(async () => { createComponent(); await triggerMembersTokenSelect([user3]); trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: emailPostData }); }); describe('when triggered from regular mounting', () => { beforeEach(() => { clickInviteButton(); }); it('calls Api inviteGroupMembers with the correct params', () => { expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, emailPostData); }); it('displays the successful toastMessage', () => { expect(showToast).toHaveBeenCalledWith('Members were successfully added'); }); it('does not call displaySuccessfulInvitationAlert on mount', () => { expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled(); }); it('does not call reloadOnInvitationSuccess', () => { expect(reloadOnInvitationSuccess).not.toHaveBeenCalled(); }); }); }); describe('when invites are not sent successfully', () => { describe('when api throws error', () => { beforeEach(async () => { jest.spyOn(axios, 'post').mockImplementation(() => { throw new Error(); }); createInviteMembersToGroupWrapper(); await triggerMembersTokenSelect([user3]); clickInviteButton(); }); it('displays the default error message', () => { expect(membersFormGroupInvalidFeedback()).toBe(INVALID_FEEDBACK_MESSAGE_DEFAULT); expect(findMembersSelect().props('exceptionState')).toBe(false); expect(findActionButton().props('loading')).toBe(false); }); }); describe('when api rejects promise', () => { beforeEach(async () => { createInviteMembersToGroupWrapper(); await triggerMembersTokenSelect([user3]); }); it('displays the api error for invalid email syntax', async () => { mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID); clickInviteButton(); await waitForPromises(); expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError); expect(findMembersSelect().props('exceptionState')).toBe(false); expect(findActionButton().props('loading')).toBe(false); }); it('clears the error when the modal is hidden', async () => { mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID); clickInviteButton(); await waitForPromises(); expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError); expect(findMembersSelect().props('exceptionState')).toBe(false); expect(findActionButton().props('loading')).toBe(false); findModal().vm.$emit('hidden'); await nextTick(); expect(findMemberErrorAlert().exists()).toBe(false); expect(membersFormGroupInvalidFeedback()).toBe(''); expect(findMembersSelect().props('exceptionState')).not.toBe(false); }); it('displays the restricted email error when restricted email is invited', async () => { mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_RESTRICTED); clickInviteButton(); await waitForPromises(); expect(findMemberErrorAlert().exists()).toBe(true); expect(findMemberErrorAlert().text()).toContain(expectedEmailRestrictedError); expect(membersFormGroupInvalidFeedback()).toBe(''); expect(findMembersSelect().props('exceptionState')).not.toBe(false); expect(findActionButton().props('loading')).toBe(false); }); it('displays all errors when there are multiple emails that return a restricted error message', async () => { mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED); clickInviteButton(); await waitForPromises(); expect(findMemberErrorAlert().exists()).toBe(true); expect(findMemberErrorAlert().text()).toContain( Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[0], ); expect(findMemberErrorAlert().text()).toContain( Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[1], ); expect(findMemberErrorAlert().text()).toContain( Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[2], ); expect(membersFormGroupInvalidFeedback()).toBe(''); expect(findMembersSelect().props('exceptionState')).not.toBe(false); }); it('displays the invalid syntax error for bad request', async () => { mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.ERROR_EMAIL_INVALID); clickInviteButton(); await waitForPromises(); expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError); expect(findMembersSelect().props('exceptionState')).toBe(false); }); it('does not call displaySuccessfulInvitationAlert on mount', () => { expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled(); }); it('does not call reloadOnInvitationSuccess', () => { expect(reloadOnInvitationSuccess).not.toHaveBeenCalled(); }); }); }); describe('when multiple emails are invited at the same time', () => { it('displays the invalid syntax error if one of the emails is invalid', async () => { createInviteMembersToGroupWrapper(); await triggerMembersTokenSelect([user3, user4]); mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.ERROR_EMAIL_INVALID); clickInviteButton(); await waitForPromises(); expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError); expect(findMembersSelect().props('exceptionState')).toBe(false); }); it('displays errors for multiple and allows clearing', async () => { createInviteMembersToGroupWrapper(); await triggerMembersTokenSelect([user3, user4, user5, user6]); mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EXPANDED_RESTRICTED); clickInviteButton(); await waitForPromises(); expect(findMemberErrorAlert().exists()).toBe(true); expect(findMemberErrorAlert().props('title')).toContain( "The following 4 members couldn't be invited", ); expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(0)); expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(1)); expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(2)); expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(3)); expect(findAccordion().exists()).toBe(true); expect(findMoreInviteErrorsButton().text()).toContain('Show more (2)'); expect(findErrorsIcon().attributes('class')).not.toContain('gl-rotate-180'); expect(findAccordion().attributes('visible')).toBeUndefined(); await findMoreInviteErrorsButton().vm.$emit('click'); expect(findMoreInviteErrorsButton().text()).toContain(EXPANDED_ERRORS); expect(findErrorsIcon().attributes('class')).toContain('gl-rotate-180'); expect(findAccordion().attributes('visible')).toBeDefined(); await findMoreInviteErrorsButton().vm.$emit('click'); expect(findMoreInviteErrorsButton().text()).toContain('Show more (2)'); expect(findAccordion().attributes('visible')).toBeUndefined(); await removeMembersToken(user3); expect(findMoreInviteErrorsButton().text()).toContain('Show more (1)'); expect(findMemberErrorAlert().props('title')).toContain( "The following 3 members couldn't be invited", ); expect(findMemberErrorAlert().text()).not.toContain(findMemberErrorMessage(0)); await removeMembersToken(user6); expect(findMoreInviteErrorsButton().exists()).toBe(false); expect(findMemberErrorAlert().props('title')).toContain( "The following 2 members couldn't be invited", ); expect(findMemberErrorAlert().text()).not.toContain(findMemberErrorMessage(2)); await removeMembersToken(user4); expect(findMemberErrorAlert().props('title')).toContain( "The following member couldn't be invited", ); expect(findMemberErrorAlert().text()).not.toContain(findMemberErrorMessage(1)); await removeMembersToken(user5); expect(findMemberErrorAlert().exists()).toBe(false); }); }); }); describe('when inviting members and non-members in same click', () => { describe('when invites are sent successfully', () => { beforeEach(async () => { createComponent(); await triggerMembersTokenSelect([user1, user3]); trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: singleUserPostData }); }); describe('when triggered from regular mounting', () => { beforeEach(async () => { await triggerOpenModal({ source: '_invite_source_' }); clickInviteButton(); }); it('calls Api inviteGroupMembers with the correct params and invite source', () => { expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, { ...singleUserPostData, invite_source: '_invite_source_', }); }); it('displays the successful toastMessage', () => { expect(showToast).toHaveBeenCalledWith('Members were successfully added'); }); it('does not call displaySuccessfulInvitationAlert on mount', () => { expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled(); }); it('does not call reloadOnInvitationSuccess', () => { expect(reloadOnInvitationSuccess).not.toHaveBeenCalled(); }); it('tracks successful invite when source is known', () => { expectTracking('invite_successful', '_invite_source_'); unmockTracking(); }); }); it('calls Apis without the invite source passed through to openModal', async () => { await triggerOpenModal(); clickInviteButton(); expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, singleUserPostData); }); }); }); }); });