diff --git a/app/src/components/modals/account/DeleteAccountModal.test.tsx b/app/src/components/modals/account/DeleteAccountModal.test.tsx
new file mode 100644
index 0000000..af4f1f2
--- /dev/null
+++ b/app/src/components/modals/account/DeleteAccountModal.test.tsx
@@ -0,0 +1,772 @@
+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 DeleteAccountModal from './DeleteAccountModal';
+
+// 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('DeleteAccountModal', () => {
+ const mockOnConfirm = vi.fn();
+ const mockOnClose = vi.fn();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockOnConfirm.mockResolvedValue(undefined);
+ mockOnClose.mockClear();
+ });
+
+ describe('Modal Visibility', () => {
+ it('renders modal when opened', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Delete Account')).toBeInTheDocument();
+ expect(
+ screen.getByText('Warning: This action cannot be undone')
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ 'Please enter your password to confirm account deletion.'
+ )
+ ).toBeInTheDocument();
+ expect(
+ screen.getByTestId('delete-account-password-input')
+ ).toBeInTheDocument();
+ expect(screen.getByTestId('cancel-delete-button')).toBeInTheDocument();
+ expect(screen.getByTestId('confirm-delete-button')).toBeInTheDocument();
+ });
+
+ it('does not render modal when closed', () => {
+ render(
+
+ );
+
+ expect(screen.queryByText('Delete Account')).not.toBeInTheDocument();
+ });
+
+ it('calls onClose when modal is closed via cancel button', () => {
+ render(
+
+ );
+
+ const cancelButton = screen.getByTestId('cancel-delete-button');
+ fireEvent.click(cancelButton);
+
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+ });
+
+ describe('Form Interaction', () => {
+ it('updates password input when typed', () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ fireEvent.change(passwordInput, { target: { value: 'testpassword123' } });
+
+ expect((passwordInput as HTMLInputElement).value).toBe('testpassword123');
+ });
+
+ it('handles form submission with valid password', async () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'validpassword' } });
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith('validpassword');
+ });
+ });
+
+ it('prevents submission with empty password', () => {
+ render(
+
+ );
+
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+ fireEvent.click(deleteButton);
+
+ // Should not call the function with empty password
+ expect(mockOnConfirm).not.toHaveBeenCalled();
+ });
+
+ it('clears input after successful submission', async () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'testpassword' } });
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith('testpassword');
+ });
+
+ await waitFor(() => {
+ expect((passwordInput as HTMLInputElement).value).toBe('');
+ });
+ });
+ });
+
+ describe('Modal Actions', () => {
+ it('has cancel and delete buttons', () => {
+ render(
+
+ );
+
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+ const cancelButton = screen.getByTestId('cancel-delete-button');
+
+ expect(deleteButton).toBeInTheDocument();
+ expect(cancelButton).toBeInTheDocument();
+
+ expect(deleteButton).toHaveRole('button');
+ expect(cancelButton).toHaveRole('button');
+ });
+
+ it('has proper button styling and colors', () => {
+ render(
+
+ );
+
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+ const cancelButton = screen.getByTestId('cancel-delete-button');
+
+ expect(deleteButton).toHaveTextContent('Delete');
+ expect(cancelButton).toHaveTextContent('Cancel');
+ });
+
+ it('closes modal when cancel button is clicked', () => {
+ render(
+
+ );
+
+ const cancelButton = screen.getByTestId('cancel-delete-button');
+ fireEvent.click(cancelButton);
+
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+
+ it('calls onConfirm when delete button is clicked with valid input', async () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'mypassword' } });
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledTimes(1);
+ expect(mockOnConfirm).toHaveBeenCalledWith('mypassword');
+ });
+ });
+ });
+
+ describe('Warning Display', () => {
+ it('displays the warning message prominently', () => {
+ render(
+
+ );
+
+ const warningElement = screen.getByText(
+ 'Warning: This action cannot be undone'
+ );
+ expect(warningElement).toBeInTheDocument();
+ });
+
+ it('displays the confirmation instructions', () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByText(
+ 'Please enter your password to confirm account deletion.'
+ )
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('Password Validation', () => {
+ it('handles various password formats', async () => {
+ const passwords = [
+ 'simple123',
+ 'Complex!Password@123',
+ 'spaces in password',
+ '12345',
+ 'very-long-password-with-many-characters-and-symbols!@#$%^&*()',
+ ];
+
+ for (const password of passwords) {
+ const { unmount } = render(
+
+ );
+
+ const passwordInput = screen.getByTestId(
+ 'delete-account-password-input'
+ );
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+
+ fireEvent.change(passwordInput, { target: { value: password } });
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith(password);
+ });
+
+ unmount();
+ vi.clearAllMocks();
+ mockOnConfirm.mockResolvedValue(undefined);
+ }
+ });
+
+ it('handles unicode characters in passwords', async () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+
+ const unicodePassword = 'パスワード123';
+ fireEvent.change(passwordInput, { target: { value: unicodePassword } });
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith(unicodePassword);
+ });
+ });
+
+ it('handles whitespace-only passwords', () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+
+ fireEvent.change(passwordInput, { target: { value: ' ' } });
+ fireEvent.click(deleteButton);
+
+ // Should not call confirm function for whitespace-only password
+ expect(mockOnConfirm).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('handles deletion errors gracefully', async () => {
+ mockOnConfirm.mockRejectedValue(new Error('Account deletion failed'));
+
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'wrongpassword' } });
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith('wrongpassword');
+ });
+
+ // Modal should handle the error gracefully (not crash)
+ expect(screen.getByText('Delete Account')).toBeInTheDocument();
+ });
+
+ it('does not clear input when deletion fails', async () => {
+ mockOnConfirm.mockRejectedValue(new Error('Invalid password'));
+
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'testpassword' } });
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith('testpassword');
+ });
+
+ // Input should retain value when deletion fails
+ expect((passwordInput as HTMLInputElement).value).toBe('testpassword');
+ });
+
+ it('handles authentication errors', async () => {
+ mockOnConfirm.mockRejectedValue(new Error('Authentication failed'));
+
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'authtest' } });
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith('authtest');
+ });
+
+ // Should not crash the component
+ expect(screen.getByText('Delete Account')).toBeInTheDocument();
+ });
+ });
+
+ describe('Accessibility', () => {
+ it('has proper form labels and structure', () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ expect(passwordInput).toBeInTheDocument();
+ expect(passwordInput.tagName).toBe('INPUT');
+ expect(passwordInput).toHaveAttribute('type', 'password');
+ });
+
+ it('has proper button roles', () => {
+ render(
+
+ );
+
+ const buttons = screen.getAllByRole('button');
+ expect(buttons.length).toBeGreaterThanOrEqual(2); // Cancel and Delete buttons
+
+ 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 passwordInput = screen.getByTestId('delete-account-password-input');
+
+ // Check that the input is focusable (not disabled or readonly)
+ expect(passwordInput).not.toHaveAttribute('disabled');
+ expect(passwordInput).not.toHaveAttribute('readonly');
+
+ // Check that the input can receive keyboard events
+ fireEvent.keyDown(passwordInput, { key: 'a' });
+ fireEvent.change(passwordInput, { target: { value: 'test' } });
+
+ expect((passwordInput as HTMLInputElement).value).toBe('test');
+
+ // Verify the input is accessible via keyboard navigation
+ expect(passwordInput).toHaveAttribute('type', 'password');
+ expect(passwordInput).toHaveAccessibleName(); // Has proper label
+ });
+
+ it('has proper modal structure', () => {
+ render(
+
+ );
+
+ // Modal should have proper title
+ expect(screen.getByText('Delete Account')).toBeInTheDocument();
+
+ // Should have form elements
+ expect(
+ screen.getByTestId('delete-account-password-input')
+ ).toBeInTheDocument();
+ });
+
+ it('has proper warning styling and visibility', () => {
+ render(
+
+ );
+
+ const warningText = screen.getByText(
+ 'Warning: This action cannot be undone'
+ );
+ expect(warningText).toBeInTheDocument();
+ });
+ });
+
+ describe('Component Props', () => {
+ it('accepts and uses onConfirm prop correctly', async () => {
+ const customMockConfirm = vi.fn().mockResolvedValue(undefined);
+
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'custompassword' } });
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(customMockConfirm).toHaveBeenCalledWith('custompassword');
+ });
+ });
+
+ it('accepts and uses onClose prop correctly', () => {
+ const customMockClose = vi.fn();
+
+ render(
+
+ );
+
+ const cancelButton = screen.getByTestId('cancel-delete-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 Account')).toBeInTheDocument();
+ });
+
+ it('handles opened prop correctly', () => {
+ const { rerender } = render(
+
+ );
+
+ // Should not be visible when opened is false
+ expect(screen.queryByText('Delete Account')).not.toBeInTheDocument();
+
+ rerender(
+
+
+
+ );
+
+ // Should be visible when opened is true
+ expect(screen.getByText('Delete Account')).toBeInTheDocument();
+ });
+ });
+
+ describe('User Interaction Flow', () => {
+ it('completes full deletion confirmation flow successfully', async () => {
+ render(
+
+ );
+
+ // 1. Modal opens and shows warning
+ expect(
+ screen.getByText('Warning: This action cannot be undone')
+ ).toBeInTheDocument();
+
+ // 2. User types password
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ fireEvent.change(passwordInput, { target: { value: 'userpassword' } });
+
+ // 3. User clicks delete
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+ fireEvent.click(deleteButton);
+
+ // 4. Confirmation function is called
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith('userpassword');
+ });
+
+ // 5. Input is cleared
+ await waitFor(() => {
+ expect((passwordInput as HTMLInputElement).value).toBe('');
+ });
+ });
+
+ it('allows user to cancel account deletion', () => {
+ render(
+
+ );
+
+ // User types password but then cancels
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ fireEvent.change(passwordInput, {
+ target: { value: 'cancelledaction' },
+ });
+
+ const cancelButton = screen.getByTestId('cancel-delete-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 passwordInput = screen.getByTestId('delete-account-password-input');
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'rapidtest' } });
+
+ // 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 Account')).toBeInTheDocument();
+ expect(mockOnConfirm).toHaveBeenCalledWith('rapidtest');
+ });
+
+ it('prevents accidental deletion with empty password', () => {
+ render(
+
+ );
+
+ // User immediately clicks delete without entering password
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+ fireEvent.click(deleteButton);
+
+ // Should not proceed with deletion
+ expect(mockOnConfirm).not.toHaveBeenCalled();
+ expect(screen.getByText('Delete Account')).toBeInTheDocument();
+ });
+ });
+
+ describe('Security Considerations', () => {
+ it('masks password input properly', () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ expect(passwordInput).toHaveAttribute('type', 'password');
+ });
+
+ it('clears password from memory after successful deletion', async () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ const deleteButton = screen.getByTestId('confirm-delete-button');
+
+ fireEvent.change(passwordInput, {
+ target: { value: 'sensitivepassword' },
+ });
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith('sensitivepassword');
+ });
+
+ // Password should be cleared from the input
+ await waitFor(() => {
+ expect((passwordInput as HTMLInputElement).value).toBe('');
+ });
+ });
+
+ it('requires explicit password confirmation', () => {
+ render(
+
+ );
+
+ // Should require password input
+ const passwordInput = screen.getByTestId('delete-account-password-input');
+ expect(passwordInput).toHaveAttribute('required');
+
+ // Should show clear warning
+ expect(
+ screen.getByText('Warning: This action cannot be undone')
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/app/src/components/modals/account/DeleteAccountModal.tsx b/app/src/components/modals/account/DeleteAccountModal.tsx
index d3e2b81..668704d 100644
--- a/app/src/components/modals/account/DeleteAccountModal.tsx
+++ b/app/src/components/modals/account/DeleteAccountModal.tsx
@@ -21,6 +21,20 @@ const DeleteAccountModal: React.FC = ({
}) => {
const [password, setPassword] = useState('');
+ const handleConfirm = async (): Promise => {
+ const trimmedPassword = password.trim();
+ if (!trimmedPassword) {
+ return;
+ }
+ try {
+ await onConfirm(trimmedPassword);
+ setPassword('');
+ } catch (error) {
+ // Keep password in case of error
+ console.error('Error confirming password:', error);
+ }
+ };
+
return (
= ({
diff --git a/app/src/components/modals/account/EmailPasswordModal.test.tsx b/app/src/components/modals/account/EmailPasswordModal.test.tsx
new file mode 100644
index 0000000..3e8103c
--- /dev/null
+++ b/app/src/components/modals/account/EmailPasswordModal.test.tsx
@@ -0,0 +1,651 @@
+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 EmailPasswordModal from './EmailPasswordModal';
+
+// 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('EmailPasswordModal', () => {
+ const mockOnConfirm = vi.fn();
+ const mockOnClose = vi.fn();
+ const testEmail = 'newemail@example.com';
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockOnConfirm.mockResolvedValue(undefined);
+ mockOnClose.mockClear();
+ });
+
+ describe('Modal Visibility', () => {
+ it('renders modal when opened', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Confirm Password')).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ `Please enter your password to confirm changing your email to: ${testEmail}`
+ )
+ ).toBeInTheDocument();
+ expect(screen.getByTestId('email-password-input')).toBeInTheDocument();
+ expect(
+ screen.getByTestId('cancel-email-password-button')
+ ).toBeInTheDocument();
+ expect(
+ screen.getByTestId('confirm-email-password-button')
+ ).toBeInTheDocument();
+ });
+
+ it('does not render modal when closed', () => {
+ render(
+
+ );
+
+ expect(screen.queryByText('Confirm Password')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('Form Interaction', () => {
+ it('updates password input when typed', () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('email-password-input');
+ fireEvent.change(passwordInput, { target: { value: 'testpassword123' } });
+
+ expect((passwordInput as HTMLInputElement).value).toBe('testpassword123');
+ });
+
+ it('handles form submission with valid password', async () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('email-password-input');
+ const confirmButton = screen.getByTestId('confirm-email-password-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'validpassword' } });
+ fireEvent.click(confirmButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith('validpassword');
+ });
+ });
+
+ it('prevents submission with empty password', () => {
+ render(
+
+ );
+
+ const confirmButton = screen.getByTestId('confirm-email-password-button');
+ fireEvent.click(confirmButton);
+
+ // Should not call the function with empty password
+ expect(mockOnConfirm).not.toHaveBeenCalled();
+ });
+
+ it('clears input after successful submission', async () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('email-password-input');
+ const confirmButton = screen.getByTestId('confirm-email-password-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'testpassword' } });
+ fireEvent.click(confirmButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith('testpassword');
+ });
+
+ await waitFor(() => {
+ expect((passwordInput as HTMLInputElement).value).toBe('');
+ });
+ });
+ });
+
+ describe('Modal Actions', () => {
+ it('has cancel and confirm buttons', () => {
+ render(
+
+ );
+
+ const confirmButton = screen.getByTestId('confirm-email-password-button');
+ const cancelButton = screen.getByTestId('cancel-email-password-button');
+
+ expect(confirmButton).toBeInTheDocument();
+ expect(cancelButton).toBeInTheDocument();
+
+ expect(confirmButton).toHaveRole('button');
+ expect(cancelButton).toHaveRole('button');
+ });
+
+ it('closes modal when cancel button is clicked', () => {
+ render(
+
+ );
+
+ const cancelButton = screen.getByTestId('cancel-email-password-button');
+ fireEvent.click(cancelButton);
+
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+
+ it('calls onConfirm when confirm button is clicked with valid input', async () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('email-password-input');
+ const confirmButton = screen.getByTestId('confirm-email-password-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'mypassword' } });
+ fireEvent.click(confirmButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledTimes(1);
+ expect(mockOnConfirm).toHaveBeenCalledWith('mypassword');
+ });
+ });
+ });
+
+ describe('Email Display', () => {
+ it('displays the correct email in the confirmation message', () => {
+ const customEmail = 'user@custom.com';
+ render(
+
+ );
+
+ expect(
+ screen.getByText(
+ `Please enter your password to confirm changing your email to: ${customEmail}`
+ )
+ ).toBeInTheDocument();
+ });
+
+ it('handles different email formats', () => {
+ const emailFormats = [
+ 'simple@example.com',
+ 'user.name@example.com',
+ 'user+tag@example.com',
+ 'very.long.email.address@domain.co.uk',
+ ];
+
+ emailFormats.forEach((email) => {
+ const { unmount } = render(
+
+ );
+
+ expect(
+ screen.getByText(
+ `Please enter your password to confirm changing your email to: ${email}`
+ )
+ ).toBeInTheDocument();
+
+ unmount();
+ });
+ });
+
+ it('handles empty email string', () => {
+ render(
+
+ );
+
+ expect(screen.getByTestId('email-password-message')).toHaveTextContent(
+ 'Please enter your password to confirm changing your email to:'
+ );
+ });
+ });
+
+ describe('Password Validation', () => {
+ it('handles various password formats', async () => {
+ const passwords = [
+ 'simple123',
+ 'Complex!Password@123',
+ 'spaces in password',
+ '12345',
+ 'very-long-password-with-many-characters-and-symbols!@#$%^&*()',
+ ];
+
+ for (const password of passwords) {
+ const { unmount } = render(
+
+ );
+
+ const passwordInput = screen.getByTestId('email-password-input');
+ const confirmButton = screen.getByTestId(
+ 'confirm-email-password-button'
+ );
+
+ fireEvent.change(passwordInput, { target: { value: password } });
+ fireEvent.click(confirmButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith(password);
+ });
+
+ unmount();
+ vi.clearAllMocks();
+ mockOnConfirm.mockResolvedValue(undefined);
+ }
+ });
+
+ it('handles unicode characters in passwords', async () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('email-password-input');
+ const confirmButton = screen.getByTestId('confirm-email-password-button');
+
+ const unicodePassword = 'パスワード123';
+ fireEvent.change(passwordInput, { target: { value: unicodePassword } });
+ fireEvent.click(confirmButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith(unicodePassword);
+ });
+ });
+
+ it('trims whitespace from passwords', async () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('email-password-input');
+ const confirmButton = screen.getByTestId('confirm-email-password-button');
+
+ fireEvent.change(passwordInput, {
+ target: { value: ' password123 ' },
+ });
+ fireEvent.click(confirmButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith('password123');
+ });
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('handles confirmation errors gracefully', async () => {
+ mockOnConfirm.mockRejectedValue(new Error('Authentication failed'));
+
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('email-password-input');
+ const confirmButton = screen.getByTestId('confirm-email-password-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'wrongpassword' } });
+ fireEvent.click(confirmButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith('wrongpassword');
+ });
+
+ // Modal should handle the error gracefully (not crash)
+ expect(screen.getByText('Confirm Password')).toBeInTheDocument();
+ });
+
+ it('does not clear input when confirmation fails', async () => {
+ mockOnConfirm.mockRejectedValue(new Error('Invalid password'));
+
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('email-password-input');
+ const confirmButton = screen.getByTestId('confirm-email-password-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'testpassword' } });
+ fireEvent.click(confirmButton);
+
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith('testpassword');
+ });
+
+ // Input should retain value when confirmation fails
+ expect((passwordInput as HTMLInputElement).value).toBe('testpassword');
+ });
+ });
+
+ describe('Accessibility', () => {
+ it('has proper form labels and structure', () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('email-password-input');
+ expect(passwordInput).toBeInTheDocument();
+ expect(passwordInput.tagName).toBe('INPUT');
+ expect(passwordInput).toHaveAttribute('type', 'password');
+ });
+
+ it('has proper button roles', () => {
+ render(
+
+ );
+
+ const buttons = screen.getAllByRole('button');
+ expect(buttons.length).toBeGreaterThanOrEqual(2); // Cancel and Confirm buttons
+
+ const cancelButton = screen.getByRole('button', { name: /cancel/i });
+ const confirmButton = screen.getByRole('button', { name: /confirm/i });
+
+ expect(cancelButton).toBeInTheDocument();
+ expect(confirmButton).toBeInTheDocument();
+ });
+
+ it('supports keyboard navigation', () => {
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('email-password-input');
+
+ // Check that the input is focusable (not disabled or readonly)
+ expect(passwordInput).not.toHaveAttribute('disabled');
+ expect(passwordInput).not.toHaveAttribute('readonly');
+
+ // Check that the input can receive keyboard events
+ fireEvent.keyDown(passwordInput, { key: 'a' });
+ fireEvent.change(passwordInput, { target: { value: 'test' } });
+
+ expect((passwordInput as HTMLInputElement).value).toBe('test');
+
+ // Verify the input is accessible via keyboard navigation
+ expect(passwordInput).toHaveAttribute('type', 'password');
+ expect(passwordInput).toHaveAccessibleName(); // Has proper label
+ });
+
+ it('has proper modal structure', () => {
+ render(
+
+ );
+
+ // Modal should have proper title
+ expect(screen.getByText('Confirm Password')).toBeInTheDocument();
+
+ // Should have form elements
+ expect(screen.getByTestId('email-password-input')).toBeInTheDocument();
+ });
+ });
+
+ describe('Component Props', () => {
+ it('accepts and uses onConfirm prop correctly', async () => {
+ const customMockConfirm = vi.fn().mockResolvedValue(undefined);
+
+ render(
+
+ );
+
+ const passwordInput = screen.getByTestId('email-password-input');
+ const confirmButton = screen.getByTestId('confirm-email-password-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'custompassword' } });
+ fireEvent.click(confirmButton);
+
+ await waitFor(() => {
+ expect(customMockConfirm).toHaveBeenCalledWith('custompassword');
+ });
+ });
+
+ it('accepts and uses onClose prop correctly', () => {
+ const customMockClose = vi.fn();
+
+ render(
+
+ );
+
+ const cancelButton = screen.getByTestId('cancel-email-password-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('Confirm Password')).toBeInTheDocument();
+ });
+ });
+
+ describe('User Interaction Flow', () => {
+ it('completes full confirmation flow successfully', async () => {
+ render(
+
+ );
+
+ // 1. Modal opens and shows email change confirmation
+ expect(
+ screen.getByText(
+ `Please enter your password to confirm changing your email to: ${testEmail}`
+ )
+ ).toBeInTheDocument();
+
+ // 2. User types password
+ const passwordInput = screen.getByTestId('email-password-input');
+ fireEvent.change(passwordInput, { target: { value: 'userpassword' } });
+
+ // 3. User clicks confirm
+ const confirmButton = screen.getByTestId('confirm-email-password-button');
+ fireEvent.click(confirmButton);
+
+ // 4. Confirmation function is called
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith('userpassword');
+ });
+
+ // 5. Input is cleared
+ await waitFor(() => {
+ expect((passwordInput as HTMLInputElement).value).toBe('');
+ });
+ });
+
+ it('allows user to cancel email change', () => {
+ render(
+
+ );
+
+ // User types password but then cancels
+ const passwordInput = screen.getByTestId('email-password-input');
+ fireEvent.change(passwordInput, {
+ target: { value: 'cancelleddaction' },
+ });
+
+ const cancelButton = screen.getByTestId('cancel-email-password-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 passwordInput = screen.getByTestId('email-password-input');
+ const confirmButton = screen.getByTestId('confirm-email-password-button');
+
+ fireEvent.change(passwordInput, { target: { value: 'rapidtest' } });
+
+ // Rapidly click multiple times - should not crash
+ fireEvent.click(confirmButton);
+ fireEvent.click(confirmButton);
+ fireEvent.click(confirmButton);
+
+ // Verify component is still functional
+ expect(screen.getByText('Confirm Password')).toBeInTheDocument();
+ expect(mockOnConfirm).toHaveBeenCalledWith('rapidtest');
+ });
+ });
+});
diff --git a/app/src/components/modals/account/EmailPasswordModal.tsx b/app/src/components/modals/account/EmailPasswordModal.tsx
index e82c92d..66f3ac8 100644
--- a/app/src/components/modals/account/EmailPasswordModal.tsx
+++ b/app/src/components/modals/account/EmailPasswordModal.tsx
@@ -11,7 +11,7 @@ import {
interface EmailPasswordModalProps {
opened: boolean;
onClose: () => void;
- onConfirm: (password: string) => Promise;
+ onConfirm: (password: string) => Promise;
email: string;
}
@@ -23,6 +23,27 @@ const EmailPasswordModal: React.FC = ({
}) => {
const [password, setPassword] = useState('');
+ async function handleConfirm(): Promise {
+ const trimmedPassword = password.trim();
+ if (!trimmedPassword) {
+ return;
+ }
+ try {
+ await onConfirm(trimmedPassword);
+ setPassword('');
+ } catch (error) {
+ // Keep password in case of error
+ console.error('Error confirming password:', error);
+ }
+ }
+
+ const handleKeyDown = (event: React.KeyboardEvent): void => {
+ if (event.key === 'Enter' && !event.shiftKey) {
+ event.preventDefault();
+ void handleConfirm();
+ }
+ };
+
return (
= ({
size="sm"
>
-
- Please enter your password to confirm changing your email to: {email}
+
+ {`Please enter your password to confirm changing your email to: ${email}`}
setPassword(e.currentTarget.value)}
required
/>
@@ -52,11 +74,9 @@ const EmailPasswordModal: React.FC = ({
Cancel
diff --git a/app/src/components/modals/file/CreateFileModal.tsx b/app/src/components/modals/file/CreateFileModal.tsx
index 655a736..499af0f 100644
--- a/app/src/components/modals/file/CreateFileModal.tsx
+++ b/app/src/components/modals/file/CreateFileModal.tsx
@@ -56,6 +56,7 @@ const CreateFileModal: React.FC = ({ onCreateFile }) => {
diff --git a/app/src/components/modals/git/CommitMessageModal.tsx b/app/src/components/modals/git/CommitMessageModal.tsx
index db9c25b..7eab8cc 100644
--- a/app/src/components/modals/git/CommitMessageModal.tsx
+++ b/app/src/components/modals/git/CommitMessageModal.tsx
@@ -60,6 +60,7 @@ const CommitMessageModal: React.FC = ({
diff --git a/app/src/components/settings/account/AccountSettings.tsx b/app/src/components/settings/account/AccountSettings.tsx
index 5464429..5538ddb 100644
--- a/app/src/components/settings/account/AccountSettings.tsx
+++ b/app/src/components/settings/account/AccountSettings.tsx
@@ -153,7 +153,7 @@ const AccountSettings: React.FC = ({
}
};
- const handleEmailConfirm = async (password: string): Promise => {
+ const handleEmailConfirm = async (password: string): Promise => {
const updates: UserProfileSettings = {
...state.localSettings,
currentPassword: password,
@@ -181,6 +181,11 @@ const AccountSettings: React.FC = ({
dispatch({ type: SettingsActionType.MARK_SAVED });
setEmailModalOpened(false);
onClose();
+ return true;
+ } else {
+ // TODO: Handle errors appropriately
+ // notifications.show({...
+ return false;
}
};