diff --git a/web/src/components/features/modal/InvitePeopleModal.test.tsx b/web/src/components/features/modal/InvitePeopleModal.test.tsx new file mode 100644 index 000000000..48e57f2ea --- /dev/null +++ b/web/src/components/features/modal/InvitePeopleModal.test.tsx @@ -0,0 +1,153 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { InvitePeopleModal } from './InvitePeopleModal'; +import { useInviteUser } from '@/api/buster_rest/users'; +import { useBusterNotifications } from '@/context/BusterNotifications'; + +// Mock the hooks +jest.mock('@/api/buster_rest/users', () => ({ + useInviteUser: jest.fn() +})); + +jest.mock('@/context/BusterNotifications', () => ({ + useBusterNotifications: jest.fn() +})); + +describe('InvitePeopleModal', () => { + const mockOnClose = jest.fn(); + const mockMutateAsync = jest.fn(); + const mockOpenErrorMessage = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + (useInviteUser as jest.Mock).mockReturnValue({ + mutateAsync: mockMutateAsync, + isPending: false + }); + (useBusterNotifications as jest.Mock).mockReturnValue({ + openErrorMessage: mockOpenErrorMessage + }); + }); + + it('renders correctly when open', () => { + render(); + + expect(screen.getByText('Invite others to join your workspace')).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/buster@bluthbananas.com/)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Send invites' })).toBeDisabled(); + }); + + it('handles valid email input', async () => { + render(); + + const input = screen.getByPlaceholderText(/buster@bluthbananas.com/); + fireEvent.change(input, { target: { value: 'test@example.com' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + + expect(screen.getByText('test@example.com')).toBeInTheDocument(); + expect(screen.getByText('Send invites')).toBeEnabled(); + }); + + it('handles invalid email input', () => { + render(); + + const input = screen.getByPlaceholderText(/buster@bluthbananas.com/); + fireEvent.change(input, { target: { value: 'invalid-email' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + + expect(mockOpenErrorMessage).toHaveBeenCalledWith('Invalid email'); + expect(screen.queryByText('invalid-email')).not.toBeInTheDocument(); + }); + + it('handles multiple email inputs', () => { + render(); + + const input = screen.getByPlaceholderText(/buster@bluthbananas.com/); + fireEvent.change(input, { target: { value: 'test1@example.com, test2@example.com' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + + expect(screen.getByText('test1@example.com')).toBeInTheDocument(); + expect(screen.getByText('test2@example.com')).toBeInTheDocument(); + }); + + it('removes email tag when clicked', async () => { + render(); + + const input = screen.getByPlaceholderText(/buster@bluthbananas.com/); + fireEvent.change(input, { target: { value: 'test@example.com' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + + const tag = screen.getByText('test@example.com').closest('[data-tag="true"]'); + expect(tag).toBeInTheDocument(); + + const removeButton = tag?.querySelector('button'); + expect(removeButton).toBeInTheDocument(); + fireEvent.pointerDown(removeButton!); + + await waitFor(() => { + expect(screen.queryByText('test@example.com')).not.toBeInTheDocument(); + }); + }); + + it('sends invites when submit button is clicked', async () => { + render(); + + const input = screen.getByPlaceholderText(/buster@bluthbananas.com/); + fireEvent.change(input, { target: { value: 'test@example.com' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + + const submitButton = screen.getByText('Send invites'); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(mockMutateAsync).toHaveBeenCalledWith({ + emails: ['test@example.com'] + }); + expect(mockOnClose).toHaveBeenCalled(); + }); + }); + + it('deduplicates email addresses', async () => { + render(); + + const input = screen.getByPlaceholderText(/buster@bluthbananas.com/); + fireEvent.change(input, { target: { value: 'test@example.com, test@example.com' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + + const submitButton = screen.getByText('Send invites'); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(mockMutateAsync).toHaveBeenCalledWith({ + emails: ['test@example.com'] + }); + }); + }); + + it('handles pasting multiple email addresses', async () => { + render(); + + const input = screen.getByPlaceholderText(/buster@bluthbananas.com/); + const pastedEmails = 'test1@example.com, test2@example.com, test3@example.com'; + + fireEvent.paste(input, { + clipboardData: { + getData: () => pastedEmails + } + }); + + expect(screen.getByText('test1@example.com')).toBeInTheDocument(); + expect(screen.getByText('test2@example.com')).toBeInTheDocument(); + expect(screen.getByText('test3@example.com')).toBeInTheDocument(); + + // Remove the first email + const firstTag = screen.getByText('test1@example.com').closest('[data-tag="true"]'); + const removeButton = firstTag?.querySelector('button'); + fireEvent.pointerDown(removeButton!); + + await waitFor(() => { + expect(screen.queryByText('test1@example.com')).not.toBeInTheDocument(); + expect(screen.getByText('test2@example.com')).toBeInTheDocument(); + expect(screen.getByText('test3@example.com')).toBeInTheDocument(); + }); + }); +}); diff --git a/web/src/components/features/modal/InvitePeopleModal.tsx b/web/src/components/features/modal/InvitePeopleModal.tsx index f10e39515..bdca6be7b 100644 --- a/web/src/components/features/modal/InvitePeopleModal.tsx +++ b/web/src/components/features/modal/InvitePeopleModal.tsx @@ -6,6 +6,7 @@ import { useInviteUser } from '@/api/buster_rest/users'; import { validate } from 'email-validator'; import { useBusterNotifications } from '@/context/BusterNotifications'; import uniq from 'lodash/uniq'; +import { timeout } from '@/lib'; export const InvitePeopleModal: React.FC<{ open: boolean; @@ -14,12 +15,20 @@ export const InvitePeopleModal: React.FC<{ const [emails, setEmails] = React.useState([]); const { mutateAsync: inviteUsers, isPending: inviting } = useInviteUser(); const [inputText, setInputText] = React.useState(''); - const { openErrorMessage } = useBusterNotifications(); + const { openErrorMessage, openSuccessMessage } = useBusterNotifications(); const handleInvite = useMemoizedFn(async () => { const allEmails = uniq([...emails, inputText].filter((email) => !!email && validate(email))); - await inviteUsers({ emails: allEmails }); - onClose(); + try { + await inviteUsers({ emails: allEmails }); + onClose(); + openSuccessMessage('Invites sent'); + await timeout(330); + setEmails([]); + setInputText(''); + } catch (error) { + openErrorMessage('Failed to invite users'); + } }); const memoizedHeader = useMemo(() => {