diff --git a/app/src/components/settings/account/AccountSettings.test.tsx b/app/src/components/settings/account/AccountSettings.test.tsx
new file mode 100644
index 0000000..cdf60ff
--- /dev/null
+++ b/app/src/components/settings/account/AccountSettings.test.tsx
@@ -0,0 +1,246 @@
+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 AccountSettings from './AccountSettings';
+
+// Mock the auth context
+const mockUser = {
+ id: 1,
+ email: 'test@example.com',
+ displayName: 'Test User',
+ role: 'editor' as const,
+};
+const mockRefreshUser = vi.fn();
+vi.mock('../../../contexts/AuthContext', () => ({
+ useAuth: () => ({
+ user: mockUser,
+ refreshUser: mockRefreshUser,
+ }),
+}));
+
+// Mock the profile settings hook
+const mockUpdateProfile = vi.fn();
+vi.mock('../../../hooks/useProfileSettings', () => ({
+ useProfileSettings: () => ({
+ loading: false,
+ updateProfile: mockUpdateProfile,
+ }),
+}));
+
+// Mock notifications
+vi.mock('@mantine/notifications', () => ({
+ notifications: {
+ show: vi.fn(),
+ },
+}));
+
+// Mock the sub-components
+vi.mock('./ProfileSettings', () => ({
+ default: ({
+ settings,
+ onInputChange,
+ }: {
+ settings: {
+ displayName?: string;
+ email?: string;
+ };
+ onInputChange: (field: string, value: string) => void;
+ }) => (
+
+ onInputChange('displayName', e.target.value)}
+ />
+ onInputChange('email', e.target.value)}
+ />
+
+ ),
+}));
+
+vi.mock('./SecuritySettings', () => ({
+ default: ({
+ settings,
+ onInputChange,
+ }: {
+ settings: {
+ currentPassword?: string;
+ newPassword?: string;
+ };
+ onInputChange: (field: string, value: string) => void;
+ }) => (
+
+ onInputChange('currentPassword', e.target.value)}
+ />
+ onInputChange('newPassword', e.target.value)}
+ />
+
+ ),
+}));
+
+vi.mock('./DangerZoneSettings', () => ({
+ default: () => Danger Zone
,
+}));
+
+vi.mock('../../modals/account/EmailPasswordModal', () => ({
+ default: ({
+ opened,
+ onConfirm,
+ }: {
+ opened: boolean;
+ onConfirm: (password: string) => void;
+ }) =>
+ opened ? (
+
+
+
+ ) : null,
+}));
+
+// 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('AccountSettings', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockUpdateProfile.mockResolvedValue(mockUser);
+ mockRefreshUser.mockResolvedValue(undefined);
+ });
+
+ it('renders modal with all sections', () => {
+ render();
+
+ expect(screen.getByText('Account Settings')).toBeInTheDocument();
+ expect(screen.getByTestId('profile-settings')).toBeInTheDocument();
+ expect(screen.getByTestId('security-settings')).toBeInTheDocument();
+ expect(screen.getByTestId('danger-zone-settings')).toBeInTheDocument();
+ });
+
+ it('shows unsaved changes badge when settings are modified', () => {
+ render();
+
+ const displayNameInput = screen.getByTestId('display-name-input');
+ fireEvent.change(displayNameInput, { target: { value: 'Updated Name' } });
+
+ expect(screen.getByText('Unsaved Changes')).toBeInTheDocument();
+ });
+
+ it('enables save button when there are changes', () => {
+ render();
+
+ const saveButton = screen.getByRole('button', { name: 'Save Changes' });
+ expect(saveButton).toBeDisabled();
+
+ const displayNameInput = screen.getByTestId('display-name-input');
+ fireEvent.change(displayNameInput, { target: { value: 'Updated Name' } });
+
+ expect(saveButton).not.toBeDisabled();
+ });
+
+ it('saves profile changes successfully', async () => {
+ const mockOnClose = vi.fn();
+ render();
+
+ const displayNameInput = screen.getByTestId('display-name-input');
+ fireEvent.change(displayNameInput, { target: { value: 'Updated Name' } });
+
+ const saveButton = screen.getByRole('button', { name: 'Save Changes' });
+ fireEvent.click(saveButton);
+
+ await waitFor(() => {
+ expect(mockUpdateProfile).toHaveBeenCalledWith(
+ expect.objectContaining({ displayName: 'Updated Name' })
+ );
+ });
+
+ await waitFor(() => {
+ expect(mockRefreshUser).toHaveBeenCalled();
+ });
+
+ await waitFor(() => {
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+ });
+
+ it('opens email confirmation modal for email changes', () => {
+ render();
+
+ const emailInput = screen.getByTestId('email-input');
+ fireEvent.change(emailInput, { target: { value: 'new@example.com' } });
+
+ const saveButton = screen.getByRole('button', { name: 'Save Changes' });
+ fireEvent.click(saveButton);
+
+ expect(screen.getByTestId('email-password-modal')).toBeInTheDocument();
+ });
+
+ it('completes email change with password confirmation', async () => {
+ const mockOnClose = vi.fn();
+ render();
+
+ const emailInput = screen.getByTestId('email-input');
+ fireEvent.change(emailInput, { target: { value: 'new@example.com' } });
+
+ const saveButton = screen.getByRole('button', { name: 'Save Changes' });
+ fireEvent.click(saveButton);
+
+ const confirmButton = screen.getByTestId('confirm-email');
+ fireEvent.click(confirmButton);
+
+ await waitFor(() => {
+ expect(mockUpdateProfile).toHaveBeenCalledWith(
+ expect.objectContaining({
+ email: 'new@example.com',
+ currentPassword: 'test-password',
+ })
+ );
+ });
+
+ await waitFor(() => {
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+ });
+
+ it('closes modal when cancel is clicked', () => {
+ const mockOnClose = vi.fn();
+ render();
+
+ const cancelButton = screen.getByRole('button', { name: 'Cancel' });
+ fireEvent.click(cancelButton);
+
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+
+ it('does not render when closed', () => {
+ render();
+
+ expect(screen.queryByText('Account Settings')).not.toBeInTheDocument();
+ });
+});
diff --git a/app/src/components/settings/account/DangerZoneSettings.test.tsx b/app/src/components/settings/account/DangerZoneSettings.test.tsx
new file mode 100644
index 0000000..349ffd4
--- /dev/null
+++ b/app/src/components/settings/account/DangerZoneSettings.test.tsx
@@ -0,0 +1,140 @@
+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 DangerZoneSettings from './DangerZoneSettings';
+
+// Mock the auth context
+const mockLogout = vi.fn();
+vi.mock('../../../contexts/AuthContext', () => ({
+ useAuth: () => ({ logout: mockLogout }),
+}));
+
+// Mock the profile settings hook
+const mockDeleteAccount = vi.fn();
+vi.mock('../../../hooks/useProfileSettings', () => ({
+ useProfileSettings: () => ({ deleteAccount: mockDeleteAccount }),
+}));
+
+// Mock the DeleteAccountModal
+vi.mock('../../modals/account/DeleteAccountModal', () => ({
+ default: ({
+ opened,
+ onClose,
+ onConfirm,
+ }: {
+ opened: boolean;
+ onClose: () => void;
+ onConfirm: (password: string) => void;
+ }) =>
+ opened ? (
+
+
+
+
+ ) : null,
+}));
+
+// 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('DangerZoneSettings', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockDeleteAccount.mockResolvedValue(true);
+ mockLogout.mockResolvedValue(undefined);
+ });
+
+ it('renders delete button with warning text', () => {
+ render();
+
+ expect(
+ screen.getByRole('button', { name: 'Delete Account' })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ 'Once you delete your account, there is no going back. Please be certain.'
+ )
+ ).toBeInTheDocument();
+ });
+
+ it('opens and closes delete modal', () => {
+ render();
+
+ const deleteButton = screen.getByRole('button', { name: 'Delete Account' });
+ fireEvent.click(deleteButton);
+
+ expect(screen.getByTestId('delete-account-modal')).toBeInTheDocument();
+
+ fireEvent.click(screen.getByTestId('modal-close'));
+ expect(
+ screen.queryByTestId('delete-account-modal')
+ ).not.toBeInTheDocument();
+ });
+
+ it('completes account deletion and logout flow', async () => {
+ render();
+
+ fireEvent.click(screen.getByRole('button', { name: 'Delete Account' }));
+ fireEvent.click(screen.getByTestId('modal-confirm'));
+
+ await waitFor(() => {
+ expect(mockDeleteAccount).toHaveBeenCalledWith('test-password');
+ });
+
+ await waitFor(() => {
+ expect(mockLogout).toHaveBeenCalled();
+ });
+
+ expect(
+ screen.queryByTestId('delete-account-modal')
+ ).not.toBeInTheDocument();
+ });
+
+ it('keeps modal open when deletion fails', async () => {
+ mockDeleteAccount.mockResolvedValue(false);
+
+ render();
+
+ fireEvent.click(screen.getByRole('button', { name: 'Delete Account' }));
+ fireEvent.click(screen.getByTestId('modal-confirm'));
+
+ await waitFor(() => {
+ expect(mockDeleteAccount).toHaveBeenCalled();
+ });
+
+ expect(screen.getByTestId('delete-account-modal')).toBeInTheDocument();
+ expect(mockLogout).not.toHaveBeenCalled();
+ });
+
+ it('allows cancellation of deletion process', () => {
+ render();
+
+ fireEvent.click(screen.getByRole('button', { name: 'Delete Account' }));
+ fireEvent.click(screen.getByTestId('modal-close'));
+
+ expect(
+ screen.queryByTestId('delete-account-modal')
+ ).not.toBeInTheDocument();
+ expect(mockDeleteAccount).not.toHaveBeenCalled();
+ });
+});
diff --git a/app/src/components/settings/account/ProfileSettings.test.tsx b/app/src/components/settings/account/ProfileSettings.test.tsx
new file mode 100644
index 0000000..124022d
--- /dev/null
+++ b/app/src/components/settings/account/ProfileSettings.test.tsx
@@ -0,0 +1,113 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render as rtlRender, screen, fireEvent } from '@testing-library/react';
+import React from 'react';
+import { MantineProvider } from '@mantine/core';
+import ProfileSettings from './ProfileSettings';
+import type { UserProfileSettings } from '@/types/models';
+
+// 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('ProfileSettings', () => {
+ const mockOnInputChange = vi.fn();
+
+ const defaultSettings: UserProfileSettings = {
+ displayName: 'John Doe',
+ email: 'john.doe@example.com',
+ currentPassword: '',
+ newPassword: '',
+ };
+
+ const emptySettings: UserProfileSettings = {
+ displayName: '',
+ email: '',
+ currentPassword: '',
+ newPassword: '',
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('renders form fields with current values', () => {
+ render(
+
+ );
+
+ const displayNameInput = screen.getByTestId('display-name-input');
+ const emailInput = screen.getByTestId('email-input');
+
+ expect(displayNameInput).toHaveValue('John Doe');
+ expect(emailInput).toHaveValue('john.doe@example.com');
+ });
+
+ it('renders with empty settings', () => {
+ render(
+
+ );
+
+ const displayNameInput = screen.getByTestId('display-name-input');
+ const emailInput = screen.getByTestId('email-input');
+
+ expect(displayNameInput).toHaveValue('');
+ expect(emailInput).toHaveValue('');
+ });
+
+ it('calls onInputChange when display name is modified', () => {
+ render(
+
+ );
+
+ const displayNameInput = screen.getByTestId('display-name-input');
+ fireEvent.change(displayNameInput, { target: { value: 'Jane Smith' } });
+
+ expect(mockOnInputChange).toHaveBeenCalledWith('displayName', 'Jane Smith');
+ });
+
+ it('calls onInputChange when email is modified', () => {
+ render(
+
+ );
+
+ const emailInput = screen.getByTestId('email-input');
+ fireEvent.change(emailInput, { target: { value: 'jane@example.com' } });
+
+ expect(mockOnInputChange).toHaveBeenCalledWith('email', 'jane@example.com');
+ });
+
+ it('has correct input types and accessibility', () => {
+ render(
+
+ );
+
+ const displayNameInput = screen.getByTestId('display-name-input');
+ const emailInput = screen.getByTestId('email-input');
+
+ expect(displayNameInput).toHaveAttribute('type', 'text');
+ expect(emailInput).toHaveAttribute('type', 'email');
+ expect(displayNameInput).toHaveAccessibleName();
+ expect(emailInput).toHaveAccessibleName();
+ });
+});
diff --git a/app/src/components/settings/account/SecuritySettings.test.tsx b/app/src/components/settings/account/SecuritySettings.test.tsx
new file mode 100644
index 0000000..8a999aa
--- /dev/null
+++ b/app/src/components/settings/account/SecuritySettings.test.tsx
@@ -0,0 +1,137 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render as rtlRender, screen, fireEvent } from '@testing-library/react';
+import React from 'react';
+import { MantineProvider } from '@mantine/core';
+import SecuritySettings from './SecuritySettings';
+import type { UserProfileSettings } from '@/types/models';
+
+// 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('SecuritySettings', () => {
+ const mockOnInputChange = vi.fn();
+
+ const defaultSettings: UserProfileSettings = {
+ displayName: 'John Doe',
+ email: 'john@example.com',
+ currentPassword: '',
+ newPassword: '',
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('renders all password fields', () => {
+ render(
+
+ );
+
+ expect(screen.getByLabelText('Current Password')).toBeInTheDocument();
+ expect(screen.getByLabelText('New Password')).toBeInTheDocument();
+ expect(screen.getByLabelText('Confirm New Password')).toBeInTheDocument();
+ });
+
+ it('calls onInputChange for current password', () => {
+ render(
+
+ );
+
+ const currentPasswordInput = screen.getByLabelText('Current Password');
+ fireEvent.change(currentPasswordInput, { target: { value: 'oldpass123' } });
+
+ expect(mockOnInputChange).toHaveBeenCalledWith(
+ 'currentPassword',
+ 'oldpass123'
+ );
+ });
+
+ it('calls onInputChange for new password', () => {
+ render(
+
+ );
+
+ const newPasswordInput = screen.getByLabelText('New Password');
+ fireEvent.change(newPasswordInput, { target: { value: 'newpass123' } });
+
+ expect(mockOnInputChange).toHaveBeenCalledWith('newPassword', 'newpass123');
+ });
+
+ it('shows error when passwords do not match', () => {
+ render(
+
+ );
+
+ const confirmPasswordInput = screen.getByLabelText('Confirm New Password');
+ fireEvent.change(confirmPasswordInput, {
+ target: { value: 'different123' },
+ });
+
+ expect(screen.getByText('Passwords do not match')).toBeInTheDocument();
+ });
+
+ it('clears error when passwords match', () => {
+ render(
+
+ );
+
+ const confirmPasswordInput = screen.getByLabelText('Confirm New Password');
+
+ // First make them not match
+ fireEvent.change(confirmPasswordInput, {
+ target: { value: 'different123' },
+ });
+ expect(screen.getByText('Passwords do not match')).toBeInTheDocument();
+
+ // Then make them match
+ fireEvent.change(confirmPasswordInput, {
+ target: { value: 'password123' },
+ });
+ expect(
+ screen.queryByText('Passwords do not match')
+ ).not.toBeInTheDocument();
+ });
+
+ it('has correct input types and help text', () => {
+ render(
+
+ );
+
+ const currentPasswordInput = screen.getByLabelText('Current Password');
+ const newPasswordInput = screen.getByLabelText('New Password');
+ const confirmPasswordInput = screen.getByLabelText('Confirm New Password');
+
+ expect(currentPasswordInput).toHaveAttribute('type', 'password');
+ expect(newPasswordInput).toHaveAttribute('type', 'password');
+ expect(confirmPasswordInput).toHaveAttribute('type', 'password');
+
+ expect(
+ screen.getByText(/Password must be at least 8 characters long/)
+ ).toBeInTheDocument();
+ });
+});