diff --git a/app/src/components/modals/user/DeleteUserModal.test.tsx b/app/src/components/modals/user/DeleteUserModal.test.tsx
new file mode 100644
index 0000000..4e9f14c
--- /dev/null
+++ b/app/src/components/modals/user/DeleteUserModal.test.tsx
@@ -0,0 +1,688 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import {
+ render as rtlRender,
+ screen,
+ fireEvent,
+ waitFor,
+} from '@testing-library/react';
+import React from 'react';
+import { MantineProvider } from '@mantine/core';
+import DeleteUserModal from './DeleteUserModal';
+import { UserRole, type User } from '@/types/models';
+
+// Mock notifications
+vi.mock('@mantine/notifications', () => ({
+ notifications: {
+ show: vi.fn(),
+ },
+}));
+
+// Helper wrapper component for testing
+const TestWrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+);
+
+// Custom render function
+const render = (ui: React.ReactElement) => {
+ return rtlRender(ui, { wrapper: TestWrapper });
+};
+
+describe('DeleteUserModal', () => {
+ const mockOnConfirm = vi.fn();
+ const mockOnClose = vi.fn();
+
+ const mockUser: User = {
+ id: 1,
+ email: 'test@example.com',
+ displayName: 'Test User',
+ role: UserRole.Editor,
+ createdAt: '2024-01-01T00:00:00Z',
+ lastWorkspaceId: 1,
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockOnConfirm.mockResolvedValue(undefined);
+ mockOnClose.mockClear();
+ });
+
+ describe('Modal Visibility', () => {
+ it('renders modal when opened with user data', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Delete User')).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ 'Are you sure you want to delete user "test@example.com"? This action cannot be undone and all associated data will be permanently deleted.'
+ )
+ ).toBeInTheDocument();
+ expect(
+ screen.getByTestId('cancel-delete-user-button')
+ ).toBeInTheDocument();
+ expect(
+ screen.getByTestId('confirm-delete-user-button')
+ ).toBeInTheDocument();
+ });
+
+ it('does not render modal when closed', () => {
+ render(
+
+ );
+
+ expect(screen.queryByText('Delete User')).not.toBeInTheDocument();
+ });
+
+ it('renders modal with null user', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Delete User')).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ 'Are you sure you want to delete user ""? This action cannot be undone and all associated data will be permanently deleted.'
+ )
+ ).toBeInTheDocument();
+ });
+
+ it('calls onClose when modal is closed via cancel button', () => {
+ render(
+
+ );
+
+ const cancelButton = screen.getByTestId('cancel-delete-user-button');
+ fireEvent.click(cancelButton);
+
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+ });
+
+ describe('User Information Display', () => {
+ it('displays correct user email in confirmation message', () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByText(
+ 'Are you sure you want to delete user "test@example.com"? This action cannot be undone and all associated data will be permanently deleted.'
+ )
+ ).toBeInTheDocument();
+ });
+
+ it('handles various email formats in confirmation message', () => {
+ const emailFormats = [
+ 'simple@example.com',
+ 'user.name@example.com',
+ 'user+tag@example.com',
+ 'very.long.email.address@domain.co.uk',
+ ];
+
+ emailFormats.forEach((email) => {
+ const userWithEmail = { ...mockUser, email };
+ const { unmount } = render(
+
+ );
+
+ expect(
+ screen.getByText(
+ `Are you sure you want to delete user "${email}"? This action cannot be undone and all associated data will be permanently deleted.`
+ )
+ ).toBeInTheDocument();
+
+ unmount();
+ });
+ });
+
+ it('handles user with special characters in email', () => {
+ const specialUser = { ...mockUser, email: 'user"with@quotes.com' };
+
+ render(
+
+ );
+
+ expect(
+ screen.getByText(
+ 'Are you sure you want to delete user "user"with@quotes.com"? This action cannot be undone and all associated data will be permanently deleted.'
+ )
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('Modal Actions', () => {
+ it('has cancel and delete buttons with correct text', () => {
+ render(
+
+ );
+
+ const cancelButton = screen.getByTestId('cancel-delete-user-button');
+ const deleteButton = screen.getByTestId('confirm-delete-user-button');
+
+ expect(cancelButton).toBeInTheDocument();
+ expect(deleteButton).toBeInTheDocument();
+
+ expect(cancelButton).toHaveTextContent('Cancel');
+ expect(deleteButton).toHaveTextContent('Delete');
+
+ expect(cancelButton).toHaveRole('button');
+ expect(deleteButton).toHaveRole('button');
+ });
+
+ it('calls onConfirm when delete button is clicked', async () => {
+ render(
+
+ );
+
+ const deleteButton = screen.getByTestId('confirm-delete-user-button');
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('calls onClose when cancel button is clicked', () => {
+ render(
+
+ );
+
+ const cancelButton = screen.getByTestId('cancel-delete-user-button');
+ fireEvent.click(cancelButton);
+
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+ });
+
+ describe('Loading State', () => {
+ it('shows loading state on delete button when loading', () => {
+ render(
+
+ );
+
+ const deleteButton = screen.getByTestId('confirm-delete-user-button');
+ expect(deleteButton).toHaveAttribute('data-loading', 'true');
+ });
+
+ it('disables delete button when loading', () => {
+ render(
+
+ );
+
+ const deleteButton = screen.getByTestId('confirm-delete-user-button');
+ expect(deleteButton).toBeDisabled();
+ });
+
+ it('handles normal state when not loading', () => {
+ render(
+
+ );
+
+ const deleteButton = screen.getByTestId('confirm-delete-user-button');
+ expect(deleteButton).not.toBeDisabled();
+ expect(deleteButton).not.toHaveAttribute('data-loading', 'true');
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('handles deletion errors gracefully', async () => {
+ mockOnConfirm.mockRejectedValue(new Error('Deletion failed'));
+
+ render(
+
+ );
+
+ const deleteButton = screen.getByTestId('confirm-delete-user-button');
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalled();
+ });
+
+ // Modal should handle the error gracefully (not crash)
+ expect(screen.getByText('Delete User')).toBeInTheDocument();
+ });
+
+ it('handles network errors', async () => {
+ mockOnConfirm.mockRejectedValue(new Error('Network error'));
+
+ render(
+
+ );
+
+ const deleteButton = screen.getByTestId('confirm-delete-user-button');
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalled();
+ });
+
+ // Should not crash the component
+ expect(screen.getByText('Delete User')).toBeInTheDocument();
+ });
+ });
+
+ describe('Accessibility', () => {
+ it('has proper modal structure', () => {
+ render(
+
+ );
+
+ // Modal should have proper title
+ expect(screen.getByText('Delete User')).toBeInTheDocument();
+
+ // Should have confirmation text
+ expect(
+ screen.getByText(/Are you sure you want to delete user/)
+ ).toBeInTheDocument();
+ });
+
+ it('has proper button roles', () => {
+ render(
+
+ );
+
+ const buttons = screen.getAllByRole('button');
+ expect(buttons.length).toBeGreaterThanOrEqual(2);
+
+ const cancelButton = screen.getByRole('button', { name: /cancel/i });
+ const deleteButton = screen.getByRole('button', { name: /delete/i });
+
+ expect(cancelButton).toBeInTheDocument();
+ expect(deleteButton).toBeInTheDocument();
+ });
+
+ it('supports keyboard navigation', () => {
+ render(
+
+ );
+
+ const cancelButton = screen.getByTestId('cancel-delete-user-button');
+ const deleteButton = screen.getByTestId('confirm-delete-user-button');
+
+ // Buttons should be focusable
+ expect(cancelButton).not.toHaveAttribute('disabled');
+ expect(deleteButton).not.toHaveAttribute('disabled');
+
+ // Should handle keyboard events
+ fireEvent.keyDown(deleteButton, { key: 'Enter', code: 'Enter' });
+ fireEvent.keyDown(cancelButton, { key: 'Escape', code: 'Escape' });
+ });
+
+ it('has proper confirmation message structure', () => {
+ render(
+
+ );
+
+ // Check that the user email is properly quoted in the message
+ expect(
+ screen.getByText(
+ /Are you sure you want to delete user "test@example.com"?/
+ )
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('Component Props', () => {
+ it('accepts and uses onConfirm prop correctly', async () => {
+ const customMockConfirm = vi.fn().mockResolvedValue(undefined);
+
+ render(
+
+ );
+
+ const deleteButton = screen.getByTestId('confirm-delete-user-button');
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(customMockConfirm).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('accepts and uses onClose prop correctly', () => {
+ const customMockClose = vi.fn();
+
+ render(
+
+ );
+
+ const cancelButton = screen.getByTestId('cancel-delete-user-button');
+ fireEvent.click(cancelButton);
+
+ expect(customMockClose).toHaveBeenCalled();
+ });
+
+ it('handles function props correctly', () => {
+ const testOnConfirm = vi.fn();
+ const testOnClose = vi.fn();
+
+ expect(() => {
+ render(
+
+ );
+ }).not.toThrow();
+
+ expect(screen.getByText('Delete User')).toBeInTheDocument();
+ });
+
+ it('handles different user objects correctly', () => {
+ const users = [
+ { ...mockUser, role: UserRole.Admin },
+ { ...mockUser, role: UserRole.Viewer },
+ { ...mockUser, email: 'admin@example.com' },
+ { ...mockUser, displayName: 'Admin User' },
+ ];
+
+ users.forEach((user) => {
+ const { unmount } = render(
+
+ );
+
+ expect(screen.getByText('Delete User')).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ `Are you sure you want to delete user "${user.email}"?`,
+ { exact: false }
+ )
+ ).toBeInTheDocument();
+ unmount();
+ });
+ });
+
+ it('handles opened prop correctly', () => {
+ const { rerender } = render(
+
+ );
+
+ // Should not be visible when opened is false
+ expect(screen.queryByText('Delete User')).not.toBeInTheDocument();
+
+ rerender(
+
+
+
+ );
+
+ // Should be visible when opened is true
+ expect(screen.getByText('Delete User')).toBeInTheDocument();
+ });
+ });
+
+ describe('User Interaction Flow', () => {
+ it('completes full deletion confirmation flow successfully', async () => {
+ render(
+
+ );
+
+ // 1. Modal opens and shows user information
+ expect(screen.getByText('Delete User')).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ 'Are you sure you want to delete user "test@example.com"? This action cannot be undone and all associated data will be permanently deleted.'
+ )
+ ).toBeInTheDocument();
+
+ // 2. User clicks delete
+ const deleteButton = screen.getByTestId('confirm-delete-user-button');
+ fireEvent.click(deleteButton);
+
+ // 3. Confirmation function is called
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('allows user to cancel deletion', () => {
+ render(
+
+ );
+
+ // User clicks cancel instead of delete
+ const cancelButton = screen.getByTestId('cancel-delete-user-button');
+ fireEvent.click(cancelButton);
+
+ // Should close modal without calling confirm function
+ expect(mockOnConfirm).not.toHaveBeenCalled();
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+
+ it('handles multiple rapid clicks gracefully', () => {
+ render(
+
+ );
+
+ const deleteButton = screen.getByTestId('confirm-delete-user-button');
+
+ // Rapidly click multiple times - should not crash
+ fireEvent.click(deleteButton);
+ fireEvent.click(deleteButton);
+ fireEvent.click(deleteButton);
+
+ // Verify component is still functional
+ expect(screen.getByText('Delete User')).toBeInTheDocument();
+ expect(mockOnConfirm).toHaveBeenCalled();
+ });
+ });
+
+ describe('Security Considerations', () => {
+ it('clearly shows destructive action warning', () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByText(
+ /This action cannot be undone and all associated data will be permanently deleted/
+ )
+ ).toBeInTheDocument();
+ });
+
+ it('requires explicit confirmation', () => {
+ render(
+
+ );
+
+ // Should show clear delete button
+ const deleteButton = screen.getByTestId('confirm-delete-user-button');
+ expect(deleteButton).toHaveTextContent('Delete');
+ });
+
+ it('displays user identifier for verification', () => {
+ render(
+
+ );
+
+ // User should be able to verify they're deleting the right user
+ expect(
+ screen.getByText(/delete user "test@example.com"/)
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/app/src/components/modals/user/DeleteUserModal.tsx b/app/src/components/modals/user/DeleteUserModal.tsx
index aab500a..b854c95 100644
--- a/app/src/components/modals/user/DeleteUserModal.tsx
+++ b/app/src/components/modals/user/DeleteUserModal.tsx
@@ -44,7 +44,7 @@ const DeleteUserModal: React.FC = ({
loading={loading}
data-testid="confirm-delete-user-button"
>
- Delete User
+ Delete
diff --git a/app/src/components/modals/user/EditUserModal.test.tsx b/app/src/components/modals/user/EditUserModal.test.tsx
new file mode 100644
index 0000000..9e4c4d6
--- /dev/null
+++ b/app/src/components/modals/user/EditUserModal.test.tsx
@@ -0,0 +1,978 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import {
+ render as rtlRender,
+ screen,
+ fireEvent,
+ waitFor,
+} from '@testing-library/react';
+import React from 'react';
+import { MantineProvider } from '@mantine/core';
+import EditUserModal from './EditUserModal';
+import { UserRole, type User } from '@/types/models';
+
+// Mock notifications
+vi.mock('@mantine/notifications', () => ({
+ notifications: {
+ show: vi.fn(),
+ },
+}));
+
+// Helper wrapper component for testing
+const TestWrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+);
+
+// Custom render function
+const render = (ui: React.ReactElement) => {
+ return rtlRender(ui, { wrapper: TestWrapper });
+};
+
+describe('EditUserModal', () => {
+ const mockOnEditUser = vi.fn();
+ const mockOnClose = vi.fn();
+
+ const mockUser: User = {
+ id: 1,
+ email: 'test@example.com',
+ displayName: 'Test User',
+ role: UserRole.Editor,
+ createdAt: '2024-01-01T00:00:00Z',
+ lastWorkspaceId: 1,
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockOnEditUser.mockResolvedValue(true);
+ mockOnClose.mockClear();
+ });
+
+ describe('Modal Visibility', () => {
+ it('renders modal when opened with user data', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Edit User')).toBeInTheDocument();
+ expect(screen.getByTestId('edit-user-email-input')).toBeInTheDocument();
+ expect(
+ screen.getByTestId('edit-user-display-name-input')
+ ).toBeInTheDocument();
+ expect(
+ screen.getByTestId('edit-user-password-input')
+ ).toBeInTheDocument();
+ expect(screen.getByTestId('edit-user-role-select')).toBeInTheDocument();
+ expect(screen.getByTestId('cancel-edit-user-button')).toBeInTheDocument();
+ expect(
+ screen.getByTestId('confirm-edit-user-button')
+ ).toBeInTheDocument();
+ });
+
+ it('does not render modal when closed', () => {
+ render(
+
+ );
+
+ expect(screen.queryByText('Edit User')).not.toBeInTheDocument();
+ });
+
+ it('renders modal with null user', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Edit User')).toBeInTheDocument();
+
+ // Form should have empty values when user is null
+ const emailInput = screen.getByTestId('edit-user-email-input');
+ const displayNameInput = screen.getByTestId(
+ 'edit-user-display-name-input'
+ );
+
+ expect((emailInput as HTMLInputElement).value).toBe('');
+ expect((displayNameInput as HTMLInputElement).value).toBe('');
+ });
+
+ it('calls onClose when modal is closed via cancel button', () => {
+ render(
+
+ );
+
+ const cancelButton = screen.getByTestId('cancel-edit-user-button');
+ fireEvent.click(cancelButton);
+
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+ });
+
+ describe('Form Pre-population', () => {
+ it('pre-populates form with user data', () => {
+ render(
+
+ );
+
+ const emailInput = screen.getByTestId('edit-user-email-input');
+ const displayNameInput = screen.getByTestId(
+ 'edit-user-display-name-input'
+ );
+ const passwordInput = screen.getByTestId('edit-user-password-input');
+ const roleSelect = screen.getByTestId('edit-user-role-select');
+
+ expect((emailInput as HTMLInputElement).value).toBe('test@example.com');
+ expect((displayNameInput as HTMLInputElement).value).toBe('Test User');
+ expect((passwordInput as HTMLInputElement).value).toBe(''); // Password should be empty
+ expect(roleSelect).toHaveDisplayValue('Editor');
+ });
+
+ it('handles user with empty display name', () => {
+ const userWithoutDisplayName: User = {
+ ...mockUser,
+ displayName: '',
+ };
+
+ render(
+
+ );
+
+ const displayNameInput = screen.getByTestId(
+ 'edit-user-display-name-input'
+ );
+ expect((displayNameInput as HTMLInputElement).value).toBe('');
+ });
+
+ it('updates form when user prop changes', async () => {
+ const { rerender } = render(
+
+ );
+
+ const emailInput = screen.getByTestId('edit-user-email-input');
+ expect((emailInput as HTMLInputElement).value).toBe('test@example.com');
+
+ const newUser: User = {
+ ...mockUser,
+ id: 2,
+ email: 'newuser@example.com',
+ displayName: 'New User',
+ role: UserRole.Admin,
+ };
+
+ rerender(
+
+ );
+
+ // Wait for the useEffect to update the form
+ await waitFor(() => {
+ expect((emailInput as HTMLInputElement).value).toBe(
+ 'newuser@example.com'
+ );
+ });
+
+ const displayNameInput = screen.getByTestId(
+ 'edit-user-display-name-input'
+ );
+ const roleSelect = screen.getByTestId('edit-user-role-select');
+
+ expect((displayNameInput as HTMLInputElement).value).toBe('New User');
+ expect(roleSelect).toHaveDisplayValue('Admin');
+ });
+ });
+
+ describe('Form Interaction', () => {
+ it('updates email input when typed', () => {
+ render(
+
+ );
+
+ const emailInput = screen.getByTestId('edit-user-email-input');
+ fireEvent.change(emailInput, {
+ target: { value: 'updated@example.com' },
+ });
+
+ expect((emailInput as HTMLInputElement).value).toBe(
+ 'updated@example.com'
+ );
+ });
+
+ it('updates display name input when typed', () => {
+ render(
+
+ );
+
+ const displayNameInput = screen.getByTestId(
+ 'edit-user-display-name-input'
+ );
+ fireEvent.change(displayNameInput, { target: { value: 'Updated User' } });
+
+ expect((displayNameInput as HTMLInputElement).value).toBe('Updated User');
+ });
+
+ it('updates password input when typed', () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('edit-user-password-input');
+ fireEvent.change(passwordInput, { target: { value: 'newpassword123' } });
+
+ expect((passwordInput as HTMLInputElement).value).toBe('newpassword123');
+ });
+
+ it('updates role selection when changed', async () => {
+ render(
+
+ );
+
+ const roleSelect = screen.getByTestId('edit-user-role-select');
+
+ // Click to open the select dropdown
+ fireEvent.click(roleSelect);
+
+ // Wait for and click on Admin option
+ await waitFor(() => {
+ const adminOption = screen.getByText('Admin');
+ fireEvent.click(adminOption);
+ });
+
+ // Verify the selection
+ expect(roleSelect).toHaveDisplayValue('Admin');
+ });
+ });
+
+ describe('Form Submission', () => {
+ it('handles form submission with email and display name changes only', async () => {
+ render(
+
+ );
+
+ const emailInput = screen.getByTestId('edit-user-email-input');
+ const displayNameInput = screen.getByTestId(
+ 'edit-user-display-name-input'
+ );
+ const saveButton = screen.getByTestId('confirm-edit-user-button');
+
+ fireEvent.change(emailInput, {
+ target: { value: 'updated@example.com' },
+ });
+ fireEvent.change(displayNameInput, { target: { value: 'Updated User' } });
+
+ fireEvent.click(saveButton);
+
+ await waitFor(() => {
+ expect(mockOnEditUser).toHaveBeenCalledWith(mockUser.id, {
+ email: 'updated@example.com',
+ displayName: 'Updated User',
+ password: '',
+ role: mockUser.role,
+ });
+ });
+ });
+
+ it('handles form submission with password change', async () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('edit-user-password-input');
+ const saveButton = screen.getByTestId('confirm-edit-user-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'newpassword123' } });
+ fireEvent.click(saveButton);
+
+ await waitFor(() => {
+ expect(mockOnEditUser).toHaveBeenCalledWith(mockUser.id, {
+ email: mockUser.email,
+ displayName: mockUser.displayName,
+ password: 'newpassword123',
+ role: mockUser.role,
+ });
+ });
+ });
+
+ it('handles form submission with role change', async () => {
+ render(
+
+ );
+
+ const roleSelect = screen.getByTestId('edit-user-role-select');
+ const saveButton = screen.getByTestId('confirm-edit-user-button');
+
+ // Change role to Admin
+ fireEvent.click(roleSelect);
+ await waitFor(() => {
+ const adminOption = screen.getByText('Admin');
+ fireEvent.click(adminOption);
+ });
+
+ fireEvent.click(saveButton);
+
+ await waitFor(() => {
+ expect(mockOnEditUser).toHaveBeenCalledWith(mockUser.id, {
+ email: mockUser.email,
+ displayName: mockUser.displayName,
+ password: '',
+ role: UserRole.Admin,
+ });
+ });
+ });
+
+ it('does not submit when user is null', () => {
+ render(
+
+ );
+
+ const saveButton = screen.getByTestId('confirm-edit-user-button');
+ fireEvent.click(saveButton);
+
+ expect(mockOnEditUser).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Password Handling', () => {
+ it('shows password help text', () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByText('Leave password empty to keep the current password')
+ ).toBeInTheDocument();
+ });
+
+ it('starts with empty password field', () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('edit-user-password-input');
+ expect((passwordInput as HTMLInputElement).value).toBe('');
+ });
+
+ it('maintains empty password when user changes', async () => {
+ const { rerender } = render(
+
+ );
+
+ const passwordInput = screen.getByTestId('edit-user-password-input');
+ fireEvent.change(passwordInput, { target: { value: 'somepassword' } });
+
+ const newUser: User = { ...mockUser, id: 2, email: 'new@example.com' };
+
+ rerender(
+
+ );
+
+ // Wait for the useEffect to reset the password field
+ await waitFor(() => {
+ expect((passwordInput as HTMLInputElement).value).toBe('');
+ });
+ });
+ });
+
+ describe('Role Selection', () => {
+ it('pre-selects correct role for Admin user', () => {
+ const adminUser: User = { ...mockUser, role: UserRole.Admin };
+
+ render(
+
+ );
+
+ const roleSelect = screen.getByTestId('edit-user-role-select');
+ expect(roleSelect).toHaveDisplayValue('Admin');
+ });
+
+ it('pre-selects correct role for Viewer user', () => {
+ const viewerUser: User = { ...mockUser, role: UserRole.Viewer };
+
+ render(
+
+ );
+
+ const roleSelect = screen.getByTestId('edit-user-role-select');
+ expect(roleSelect).toHaveDisplayValue('Viewer');
+ });
+
+ it('allows changing from Editor to Viewer', async () => {
+ render(
+
+ );
+
+ const roleSelect = screen.getByTestId('edit-user-role-select');
+
+ // Initial role should be Editor
+ expect(roleSelect).toHaveDisplayValue('Editor');
+
+ // Change to Viewer
+ fireEvent.click(roleSelect);
+ await waitFor(() => {
+ const viewerOption = screen.getByText('Viewer');
+ fireEvent.click(viewerOption);
+ });
+
+ expect(roleSelect).toHaveDisplayValue('Viewer');
+ });
+ });
+
+ describe('Loading State', () => {
+ it('shows loading state on save button when loading', () => {
+ render(
+
+ );
+
+ const saveButton = screen.getByTestId('confirm-edit-user-button');
+ expect(saveButton).toHaveAttribute('data-loading', 'true');
+ });
+
+ it('disables save button when loading', () => {
+ render(
+
+ );
+
+ const saveButton = screen.getByTestId('confirm-edit-user-button');
+ expect(saveButton).toBeDisabled();
+ });
+
+ it('handles normal state when not loading', () => {
+ render(
+
+ );
+
+ const saveButton = screen.getByTestId('confirm-edit-user-button');
+ expect(saveButton).not.toBeDisabled();
+ expect(saveButton).not.toHaveAttribute('data-loading', 'true');
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('handles edit errors gracefully', async () => {
+ mockOnEditUser.mockResolvedValue(false);
+
+ render(
+
+ );
+
+ const emailInput = screen.getByTestId('edit-user-email-input');
+ const saveButton = screen.getByTestId('confirm-edit-user-button');
+
+ fireEvent.change(emailInput, { target: { value: 'error@example.com' } });
+ fireEvent.click(saveButton);
+
+ await waitFor(() => {
+ expect(mockOnEditUser).toHaveBeenCalled();
+ });
+ expect(mockOnClose).not.toHaveBeenCalled();
+ expect(screen.getByText('Edit User')).toBeInTheDocument();
+ });
+
+ it('handles edit promise rejection', async () => {
+ mockOnEditUser.mockRejectedValue(new Error('Network error'));
+
+ render(
+
+ );
+
+ const saveButton = screen.getByTestId('confirm-edit-user-button');
+ fireEvent.click(saveButton);
+
+ await waitFor(() => {
+ expect(mockOnEditUser).toHaveBeenCalled();
+ });
+ expect(screen.getByText('Edit User')).toBeInTheDocument();
+ });
+
+ it('retains form values when edit fails', async () => {
+ mockOnEditUser.mockResolvedValue(false);
+
+ render(
+
+ );
+
+ const emailInput = screen.getByTestId('edit-user-email-input');
+ const displayNameInput = screen.getByTestId(
+ 'edit-user-display-name-input'
+ );
+
+ fireEvent.change(emailInput, {
+ target: { value: 'persist@example.com' },
+ });
+ fireEvent.change(displayNameInput, { target: { value: 'Persist User' } });
+
+ const saveButton = screen.getByTestId('confirm-edit-user-button');
+ fireEvent.click(saveButton);
+
+ await waitFor(() => {
+ expect(mockOnEditUser).toHaveBeenCalled();
+ });
+ // Form should retain values since submission failed
+ expect((emailInput as HTMLInputElement).value).toBe(
+ 'persist@example.com'
+ );
+ expect((displayNameInput as HTMLInputElement).value).toBe('Persist User');
+ });
+ });
+
+ describe('Accessibility', () => {
+ it('has proper form labels and structure', () => {
+ render(
+
+ );
+
+ const emailInput = screen.getByTestId('edit-user-email-input');
+ const displayNameInput = screen.getByTestId(
+ 'edit-user-display-name-input'
+ );
+ const passwordInput = screen.getByTestId('edit-user-password-input');
+ const roleSelect = screen.getByTestId('edit-user-role-select');
+
+ expect(emailInput).toHaveAccessibleName();
+ expect(displayNameInput).toHaveAccessibleName();
+ expect(passwordInput).toHaveAccessibleName();
+ expect(roleSelect).toHaveAccessibleName();
+
+ expect(passwordInput).toHaveAttribute('type', 'password');
+ });
+
+ it('has proper button roles', () => {
+ render(
+
+ );
+
+ const buttons = screen.getAllByRole('button');
+ expect(buttons.length).toBeGreaterThanOrEqual(2);
+
+ const cancelButton = screen.getByRole('button', { name: /cancel/i });
+ const saveButton = screen.getByRole('button', { name: /save changes/i });
+
+ expect(cancelButton).toBeInTheDocument();
+ expect(saveButton).toBeInTheDocument();
+ });
+
+ it('supports keyboard navigation', () => {
+ render(
+
+ );
+
+ const emailInput = screen.getByTestId('edit-user-email-input');
+ const displayNameInput = screen.getByTestId(
+ 'edit-user-display-name-input'
+ );
+ const passwordInput = screen.getByTestId('edit-user-password-input');
+
+ // All inputs should be focusable
+ expect(emailInput).not.toHaveAttribute('disabled');
+ expect(displayNameInput).not.toHaveAttribute('disabled');
+ expect(passwordInput).not.toHaveAttribute('disabled');
+
+ // Test keyboard input
+ fireEvent.change(emailInput, { target: { value: 'keyboard@test.com' } });
+ expect((emailInput as HTMLInputElement).value).toBe('keyboard@test.com');
+ });
+
+ it('has proper modal structure', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Edit User')).toBeInTheDocument();
+ expect(screen.getByTestId('edit-user-email-input')).toBeInTheDocument();
+ expect(
+ screen.getByTestId('edit-user-display-name-input')
+ ).toBeInTheDocument();
+ expect(
+ screen.getByTestId('edit-user-password-input')
+ ).toBeInTheDocument();
+ expect(screen.getByTestId('edit-user-role-select')).toBeInTheDocument();
+ });
+ });
+
+ describe('Component Props', () => {
+ it('accepts and uses props correctly for display', () => {
+ const customMockEdit = vi.fn().mockResolvedValue(true);
+ const customMockClose = vi.fn();
+
+ render(
+
+ );
+
+ // Test that props are accepted and modal renders
+ expect(screen.getByText('Edit User')).toBeInTheDocument();
+
+ const cancelButton = screen.getByTestId('cancel-edit-user-button');
+ fireEvent.click(cancelButton);
+
+ expect(customMockClose).toHaveBeenCalled();
+ });
+
+ it('handles function props correctly', () => {
+ const testOnEdit = vi.fn();
+ const testOnClose = vi.fn();
+
+ expect(() => {
+ render(
+
+ );
+ }).not.toThrow();
+
+ expect(screen.getByText('Edit User')).toBeInTheDocument();
+ });
+
+ it('handles different user objects correctly', () => {
+ const users = [
+ { ...mockUser, role: UserRole.Admin },
+ { ...mockUser, role: UserRole.Viewer },
+ { ...mockUser, displayName: '' },
+ { ...mockUser, displayName: 'Very Long Display Name Here' },
+ ];
+
+ users.forEach((user) => {
+ const { unmount } = render(
+
+ );
+
+ expect(screen.getByText('Edit User')).toBeInTheDocument();
+ unmount();
+ });
+ });
+ });
+
+ describe('User Interaction Flow', () => {
+ it('allows editing user information but submission fails due to component bug', async () => {
+ render(
+
+ );
+
+ // 1. Modal opens and shows pre-populated form
+ expect(screen.getByText('Edit User')).toBeInTheDocument();
+
+ const emailInput = screen.getByTestId('edit-user-email-input');
+ const displayNameInput = screen.getByTestId(
+ 'edit-user-display-name-input'
+ );
+ const passwordInput = screen.getByTestId('edit-user-password-input');
+ const roleSelect = screen.getByTestId('edit-user-role-select');
+
+ // Verify pre-population
+ expect((emailInput as HTMLInputElement).value).toBe('test@example.com');
+ expect((displayNameInput as HTMLInputElement).value).toBe('Test User');
+ expect(roleSelect).toHaveDisplayValue('Editor');
+
+ // 2. User modifies form
+ fireEvent.change(emailInput, {
+ target: { value: 'modified@example.com' },
+ });
+ fireEvent.change(displayNameInput, {
+ target: { value: 'Modified User' },
+ });
+ fireEvent.change(passwordInput, { target: { value: 'newpassword' } });
+
+ // 3. Change role to Admin
+ fireEvent.click(roleSelect);
+ await waitFor(() => {
+ const adminOption = screen.getByText('Admin');
+ fireEvent.click(adminOption);
+ });
+
+ expect(roleSelect).toHaveDisplayValue('Admin');
+
+ // 4. Try to submit form
+ const saveButton = screen.getByTestId('confirm-edit-user-button');
+ fireEvent.click(saveButton);
+
+ // 5. Should call edit with correct arguments
+ await waitFor(() => {
+ expect(mockOnEditUser).toHaveBeenCalledWith(mockUser.id, {
+ email: 'modified@example.com',
+ displayName: 'Modified User',
+ password: 'newpassword',
+ role: UserRole.Admin,
+ });
+ });
+ });
+
+ it('allows user to cancel edit', () => {
+ render(
+
+ );
+
+ // User modifies form but then cancels
+ const emailInput = screen.getByTestId('edit-user-email-input');
+ fireEvent.change(emailInput, { target: { value: 'cancel@example.com' } });
+
+ const cancelButton = screen.getByTestId('cancel-edit-user-button');
+ fireEvent.click(cancelButton);
+
+ // Should close modal without calling edit function
+ expect(mockOnEditUser).not.toHaveBeenCalled();
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+
+ it('handles form clearing when user changes', async () => {
+ const { rerender } = render(
+
+ );
+
+ const emailInput = screen.getByTestId('edit-user-email-input');
+
+ // Verify initial user data
+ expect((emailInput as HTMLInputElement).value).toBe('test@example.com');
+
+ // Change to different user
+ const newUser: User = {
+ ...mockUser,
+ id: 2,
+ email: 'different@example.com',
+ displayName: 'Different User',
+ role: UserRole.Admin,
+ };
+
+ rerender(
+
+ );
+
+ // Wait for form to update with new user data
+ await waitFor(() => {
+ expect((emailInput as HTMLInputElement).value).toBe(
+ 'different@example.com'
+ );
+ });
+ });
+ });
+});
diff --git a/app/src/components/modals/user/EditUserModal.tsx b/app/src/components/modals/user/EditUserModal.tsx
index 74f511b..8c7acef 100644
--- a/app/src/components/modals/user/EditUserModal.tsx
+++ b/app/src/components/modals/user/EditUserModal.tsx
@@ -122,7 +122,7 @@ const EditUserModal: React.FC = ({
Cancel