Files
2024-02-15 17:01:50 +00:00

367 lines
12 KiB
TypeScript

import type {ComponentProps} from 'react';
import selectEvent from 'react-select-event';
import styled from '@emotion/styled';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {TeamFixture} from 'sentry-fixture/team';
import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
import {textWithMarkupMatcher} from 'sentry-test/utils';
import {makeCloseButton} from 'sentry/components/globalModal/components';
import InviteMembersModal from 'sentry/components/modals/inviteMembersModal';
import {ORG_ROLES} from 'sentry/constants';
import TeamStore from 'sentry/stores/teamStore';
import useOrganization from 'sentry/utils/useOrganization';
jest.mock('sentry/utils/useOrganization');
describe('InviteMembersModal', function () {
const team = TeamFixture();
const org = OrganizationFixture({access: ['member:write'], teams: [team]});
TeamStore.loadInitialData([team]);
const styledWrapper = styled(c => c.children);
const modalProps: ComponentProps<typeof InviteMembersModal> = {
Body: styledWrapper(),
Header: p => <span>{p.children}</span>,
Footer: styledWrapper(),
closeModal: () => {},
CloseButton: makeCloseButton(() => {}),
};
const noWriteOrg = OrganizationFixture({
access: [],
});
const roles = [
{
id: 'admin',
name: 'Admin',
desc: 'This is the admin role',
allowed: true,
},
{
id: 'member',
name: 'Member',
desc: 'This is the member role',
allowed: true,
},
];
beforeEach(function () {
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
url: `/organizations/${org.slug}/members/me/`,
method: 'GET',
body: {roles},
});
});
it('renders', async function () {
jest.mocked(useOrganization).mockReturnValue(org);
render(<InviteMembersModal {...modalProps} />);
await waitFor(() => {
// Starts with one invite row
expect(screen.getByRole('listitem')).toBeInTheDocument();
});
// We have two roles loaded from the members/me endpoint, defaulting to the
// 'member' role.
await userEvent.click(screen.getByRole('textbox', {name: 'Role'}));
expect(screen.getAllByRole('menuitemradio')).toHaveLength(roles.length);
expect(screen.getByRole('menuitemradio', {name: 'Member'})).toBeChecked();
});
it('renders for superuser', async function () {
jest.mock('sentry/utils/isActiveSuperuser', () => ({
isActiveSuperuser: jest.fn(),
}));
MockApiClient.addMockResponse({
url: `/organizations/${org.slug}/members/me/`,
method: 'GET',
status: 404,
});
jest.mocked(useOrganization).mockReturnValue(org);
render(<InviteMembersModal {...modalProps} />);
await waitFor(() => {
// Starts with one invite row
expect(screen.getByRole('listitem')).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('textbox', {name: 'Role'}));
expect(screen.getAllByRole('menuitemradio')).toHaveLength(ORG_ROLES.length);
expect(screen.getByRole('menuitemradio', {name: 'Member'})).toBeChecked();
});
it('renders without organization.access', async function () {
const organization = OrganizationFixture({access: undefined});
jest.mocked(useOrganization).mockReturnValue(organization);
render(<InviteMembersModal {...modalProps} />);
await waitFor(() => {
expect(screen.getByRole('listitem')).toBeInTheDocument();
});
});
it('can add a second row', async function () {
jest.mocked(useOrganization).mockReturnValue(org);
render(<InviteMembersModal {...modalProps} />);
await waitFor(() => {
expect(screen.getByRole('listitem')).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('button', {name: 'Add another'}));
expect(screen.getAllByRole('listitem')).toHaveLength(2);
});
it('errors on duplicate emails', async function () {
jest.mocked(useOrganization).mockReturnValue(org);
render(<InviteMembersModal {...modalProps} />);
await waitFor(() => {
expect(screen.getByRole('button', {name: 'Add another'})).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('button', {name: 'Add another'}));
const emailInputs = screen.getAllByRole('textbox', {name: 'Email Addresses'});
await userEvent.type(emailInputs[0], '[email protected]');
await userEvent.tab();
await userEvent.type(emailInputs[1], '[email protected]');
await userEvent.tab();
expect(screen.getByText('Duplicate emails between invite rows.')).toBeInTheDocument();
});
it('indicates the total invites on the invite button', async function () {
jest.mocked(useOrganization).mockReturnValue(org);
render(<InviteMembersModal {...modalProps} />);
await waitFor(() => {
expect(screen.getByRole('textbox', {name: 'Email Addresses'})).toBeInTheDocument();
});
const emailInput = screen.getByRole('textbox', {name: 'Email Addresses'});
await userEvent.type(emailInput, '[email protected]');
await userEvent.tab();
await userEvent.type(emailInput, '[email protected]');
await userEvent.tab();
expect(screen.getByRole('button', {name: 'Send invites (2)'})).toBeInTheDocument();
});
it('can be closed', async function () {
jest.mocked(useOrganization).mockReturnValue(org);
const close = jest.fn();
render(<InviteMembersModal {...modalProps} closeModal={close} />);
await waitFor(() => {
expect(screen.getByRole('button', {name: 'Cancel'})).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('button', {name: 'Cancel'}));
expect(close).toHaveBeenCalled();
});
it('sends all successful invites', async function () {
jest.mocked(useOrganization).mockReturnValue(org);
const createMemberMock = MockApiClient.addMockResponse({
url: `/organizations/${org.slug}/members/`,
method: 'POST',
});
render(<InviteMembersModal {...modalProps} />);
await waitFor(() => {
expect(screen.getByRole('button', {name: 'Add another'})).toBeInTheDocument();
});
// Setup two rows, one email each, the first with a admin role.
await userEvent.click(screen.getByRole('button', {name: 'Add another'}));
const emailInputs = screen.getAllByRole('textbox', {name: 'Email Addresses'});
const roleInputs = screen.getAllByRole('textbox', {name: 'Role'});
const teamInputs = screen.getAllByRole('textbox', {name: 'Add to Team'});
await userEvent.type(emailInputs[0], '[email protected]');
await userEvent.tab();
await selectEvent.select(roleInputs[0], 'Admin');
await selectEvent.select(teamInputs[0], '#team-slug');
await userEvent.type(emailInputs[1], '[email protected]');
await userEvent.tab();
await userEvent.click(screen.getByRole('button', {name: 'Send invites (2)'}));
// Verify data sent to the backend
expect(createMemberMock).toHaveBeenCalledTimes(2);
expect(createMemberMock).toHaveBeenNthCalledWith(
1,
`/organizations/${org.slug}/members/`,
expect.objectContaining({
data: {email: '[email protected]', role: 'admin', teams: ['team-slug']},
})
);
expect(createMemberMock).toHaveBeenNthCalledWith(
2,
`/organizations/${org.slug}/members/`,
expect.objectContaining({
data: {email: '[email protected]', role: 'member', teams: []},
})
);
// Wait for them to finish
expect(
await screen.findByText(textWithMarkupMatcher('Sent 2 invites'))
).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Close'})).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Send more invites'})).toBeInTheDocument();
// Send more reset the modal
await userEvent.click(screen.getByRole('button', {name: 'Send more invites'}));
expect(screen.getByRole('button', {name: 'Send invite'})).toBeDisabled();
});
it('marks failed invites', async function () {
jest.mocked(useOrganization).mockReturnValue(org);
const faildCreateMemberMock = MockApiClient.addMockResponse({
url: `/organizations/${org.slug}/members/`,
method: 'POST',
statusCode: 400,
});
render(<InviteMembersModal {...modalProps} />);
await waitFor(() => {
expect(screen.getByRole('textbox', {name: 'Email Addresses'})).toBeInTheDocument();
});
await userEvent.type(
screen.getByRole('textbox', {name: 'Email Addresses'}),
'bademail'
);
await userEvent.tab();
await userEvent.click(screen.getByRole('button', {name: 'Send invite'}));
expect(faildCreateMemberMock).toHaveBeenCalled();
expect(
await screen.findByText(textWithMarkupMatcher('Sent 0 invites, 1 failed to send.'))
).toBeInTheDocument();
});
it('can send initial email', async function () {
jest.mocked(useOrganization).mockReturnValue(org);
const createMemberMock = MockApiClient.addMockResponse({
url: `/organizations/${org.slug}/members/`,
method: 'POST',
});
const initialEmail = '[email protected]';
const initialData = [{emails: new Set([initialEmail])}];
render(<InviteMembersModal {...modalProps} initialData={initialData} />);
await waitFor(() => {
expect(screen.getByText(initialEmail)).toBeInTheDocument();
});
// Just immediately click send
await userEvent.click(screen.getByRole('button', {name: 'Send invite'}));
expect(createMemberMock).toHaveBeenCalledWith(
`/organizations/${org.slug}/members/`,
expect.objectContaining({
data: {email: initialEmail, role: 'member', teams: []},
})
);
expect(
await screen.findByText(textWithMarkupMatcher('Sent 1 invite'))
).toBeInTheDocument();
});
it('can send initial email with role and team', async function () {
jest.mocked(useOrganization).mockReturnValue(org);
const createMemberMock = MockApiClient.addMockResponse({
url: `/organizations/${org.slug}/members/`,
method: 'POST',
});
const initialEmail = '[email protected]';
const role = 'admin';
const initialData = [
{emails: new Set([initialEmail]), role, teams: new Set([team.slug])},
];
render(<InviteMembersModal {...modalProps} initialData={initialData} />);
await waitFor(() => {
expect(screen.getByRole('button', {name: 'Send invite'})).toBeInTheDocument();
});
// Just immediately click send
await userEvent.click(screen.getByRole('button', {name: 'Send invite'}));
expect(screen.getByText(initialEmail)).toBeInTheDocument();
expect(screen.getByText('Admin')).toBeInTheDocument();
expect(createMemberMock).toHaveBeenCalledWith(
`/organizations/${org.slug}/members/`,
expect.objectContaining({
data: {email: initialEmail, role, teams: [team.slug]},
})
);
expect(
await screen.findByText(textWithMarkupMatcher('Sent 1 invite'))
).toBeInTheDocument();
});
describe('member invite request mode', function () {
it('has adjusted wording', async function () {
jest.mocked(useOrganization).mockReturnValue(noWriteOrg);
render(<InviteMembersModal {...modalProps} />);
await waitFor(() => {
expect(
screen.getByRole('button', {name: 'Send invite request'})
).toBeInTheDocument();
});
});
it('POSTS to the invite-request endpoint', async function () {
jest.mocked(useOrganization).mockReturnValue(noWriteOrg);
const createInviteRequestMock = MockApiClient.addMockResponse({
url: `/organizations/${org.slug}/invite-requests/`,
method: 'POST',
});
// Use initial data so we don't have to setup as much stuff
const initialEmail = '[email protected]';
const initialData = [{emails: new Set(['[email protected]'])}];
render(<InviteMembersModal {...modalProps} initialData={initialData} />);
await waitFor(() => {
expect(screen.getByText(initialEmail)).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('button', {name: 'Send invite request'}));
expect(createInviteRequestMock).toHaveBeenCalledTimes(1);
});
});
});