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(() => {