;
+ workspaceName: string | undefined;
+ }) =>
+ opened ? (
+
+ {workspaceName}
+
+
+
+ ) : 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 (Workspace)', () => {
+ beforeEach(async () => {
+ vi.clearAllMocks();
+ mockDeleteCurrentWorkspace.mockResolvedValue(undefined);
+
+ const { useWorkspace } = await import('../../../hooks/useWorkspace');
+ vi.mocked(useWorkspace).mockReturnValue({
+ currentWorkspace: {
+ id: 1,
+ userId: 1,
+ name: 'Test Workspace',
+ createdAt: '2024-01-01T00:00:00Z',
+ theme: Theme.Light,
+ autoSave: false,
+ showHiddenFiles: false,
+ gitEnabled: false,
+ gitUrl: '',
+ gitUser: '',
+ gitToken: '',
+ gitAutoCommit: false,
+ gitCommitMsgTemplate: '',
+ gitCommitName: '',
+ gitCommitEmail: '',
+ },
+ workspaces: [
+ {
+ id: 1,
+ userId: 1,
+ name: 'Workspace 1',
+ createdAt: '2024-01-01T00:00:00Z',
+ theme: Theme.Light,
+ autoSave: false,
+ showHiddenFiles: false,
+ gitEnabled: false,
+ gitUrl: '',
+ gitUser: '',
+ gitToken: '',
+ gitAutoCommit: false,
+ gitCommitMsgTemplate: '',
+ gitCommitName: '',
+ gitCommitEmail: '',
+ },
+ {
+ id: 2,
+ userId: 1,
+ name: 'Workspace 2',
+ createdAt: '2024-01-01T00:00:00Z',
+ theme: Theme.Light,
+ autoSave: false,
+ showHiddenFiles: false,
+ gitEnabled: false,
+ gitUrl: '',
+ gitUser: '',
+ gitToken: '',
+ gitAutoCommit: false,
+ gitCommitMsgTemplate: '',
+ gitCommitName: '',
+ gitCommitEmail: '',
+ },
+ ],
+ settings: {
+ id: 1,
+ userId: 1,
+ name: 'Test Workspace',
+ createdAt: '2024-01-01T00:00:00Z',
+ theme: Theme.Light,
+ autoSave: false,
+ showHiddenFiles: false,
+ gitEnabled: false,
+ gitUrl: '',
+ gitUser: '',
+ gitToken: '',
+ gitAutoCommit: false,
+ gitCommitMsgTemplate: '',
+ gitCommitName: '',
+ gitCommitEmail: '',
+ },
+ updateSettings: vi.fn(),
+ loading: false,
+ colorScheme: 'light',
+ updateColorScheme: vi.fn(),
+ switchWorkspace: vi.fn(),
+ deleteCurrentWorkspace: mockDeleteCurrentWorkspace,
+ });
+ });
+
+ it('renders delete button when multiple workspaces exist', () => {
+ render();
+
+ const deleteButton = screen.getByRole('button', {
+ name: 'Delete Workspace',
+ });
+ expect(deleteButton).toBeInTheDocument();
+ expect(deleteButton).not.toBeDisabled();
+ });
+
+ it('disables delete button when only one workspace exists', async () => {
+ const { useWorkspace } = await import('../../../hooks/useWorkspace');
+ vi.mocked(useWorkspace).mockReturnValue({
+ currentWorkspace: {
+ id: 1,
+ userId: 1,
+ name: 'Last Workspace',
+ createdAt: '2024-01-01T00:00:00Z',
+ theme: Theme.Light,
+ autoSave: false,
+ showHiddenFiles: false,
+ gitEnabled: false,
+ gitUrl: '',
+ gitUser: '',
+ gitToken: '',
+ gitAutoCommit: false,
+ gitCommitMsgTemplate: '',
+ gitCommitName: '',
+ gitCommitEmail: '',
+ },
+ workspaces: [
+ {
+ id: 1,
+ userId: 1,
+ name: 'Last Workspace',
+ createdAt: '2024-01-01T00:00:00Z',
+ theme: Theme.Light,
+ autoSave: false,
+ showHiddenFiles: false,
+ gitEnabled: false,
+ gitUrl: '',
+ gitUser: '',
+ gitToken: '',
+ gitAutoCommit: false,
+ gitCommitMsgTemplate: '',
+ gitCommitName: '',
+ gitCommitEmail: '',
+ },
+ ],
+ settings: {
+ id: 1,
+ userId: 1,
+ name: 'Last Workspace',
+ createdAt: '2024-01-01T00:00:00Z',
+ theme: Theme.Light,
+ autoSave: false,
+ showHiddenFiles: false,
+ gitEnabled: false,
+ gitUrl: '',
+ gitUser: '',
+ gitToken: '',
+ gitAutoCommit: false,
+ gitCommitMsgTemplate: '',
+ gitCommitName: '',
+ gitCommitEmail: '',
+ },
+ updateSettings: vi.fn(),
+ loading: false,
+ colorScheme: 'light',
+ updateColorScheme: vi.fn(),
+ switchWorkspace: vi.fn(),
+ deleteCurrentWorkspace: mockDeleteCurrentWorkspace,
+ });
+
+ render();
+
+ const deleteButton = screen.getByRole('button', {
+ name: 'Delete Workspace',
+ });
+ expect(deleteButton).toBeDisabled();
+ expect(deleteButton).toHaveAttribute(
+ 'title',
+ 'Cannot delete the last workspace'
+ );
+ });
+
+ it('opens and closes delete modal', () => {
+ render();
+
+ const deleteButton = screen.getByRole('button', {
+ name: 'Delete Workspace',
+ });
+ fireEvent.click(deleteButton);
+
+ expect(screen.getByTestId('delete-workspace-modal')).toBeInTheDocument();
+ expect(screen.getByTestId('workspace-name')).toHaveTextContent(
+ 'Test Workspace'
+ );
+
+ fireEvent.click(screen.getByTestId('modal-close'));
+ expect(
+ screen.queryByTestId('delete-workspace-modal')
+ ).not.toBeInTheDocument();
+ });
+
+ it('completes workspace deletion flow', async () => {
+ render();
+
+ fireEvent.click(screen.getByRole('button', { name: 'Delete Workspace' }));
+ fireEvent.click(screen.getByTestId('modal-confirm'));
+
+ await waitFor(() => {
+ expect(mockDeleteCurrentWorkspace).toHaveBeenCalled();
+ });
+
+ await waitFor(() => {
+ expect(mockSetSettingsModalVisible).toHaveBeenCalledWith(false);
+ });
+
+ expect(
+ screen.queryByTestId('delete-workspace-modal')
+ ).not.toBeInTheDocument();
+ });
+
+ it('allows cancellation of deletion process', () => {
+ render();
+
+ fireEvent.click(screen.getByRole('button', { name: 'Delete Workspace' }));
+ fireEvent.click(screen.getByTestId('modal-close'));
+
+ expect(
+ screen.queryByTestId('delete-workspace-modal')
+ ).not.toBeInTheDocument();
+ expect(mockDeleteCurrentWorkspace).not.toHaveBeenCalled();
+ expect(mockSetSettingsModalVisible).not.toHaveBeenCalled();
+ });
+});
diff --git a/app/src/components/settings/workspace/EditorSettings.test.tsx b/app/src/components/settings/workspace/EditorSettings.test.tsx
new file mode 100644
index 0000000..3dc1d40
--- /dev/null
+++ b/app/src/components/settings/workspace/EditorSettings.test.tsx
@@ -0,0 +1,78 @@
+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 EditorSettings from './EditorSettings';
+
+// 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('EditorSettings', () => {
+ const mockOnAutoSaveChange = vi.fn();
+ const mockOnShowHiddenFilesChange = vi.fn();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('renders both toggle switches with labels', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Auto Save')).toBeInTheDocument();
+ expect(screen.getByText('Show Hidden Files')).toBeInTheDocument();
+ });
+
+ it('shows correct toggle states', () => {
+ render(
+
+ );
+
+ const toggles = screen.getAllByRole('switch');
+ const autoSaveToggle = toggles[0];
+ const hiddenFilesToggle = toggles[1];
+
+ expect(autoSaveToggle).toBeChecked();
+ expect(hiddenFilesToggle).not.toBeChecked();
+ });
+
+ it('calls onShowHiddenFilesChange when toggle is clicked', () => {
+ render(
+
+ );
+
+ // Get the show hidden files toggle by finding the one that's not disabled
+ const toggles = screen.getAllByRole('switch');
+ const hiddenFilesToggle = toggles.find(
+ (toggle) => !toggle.hasAttribute('disabled')
+ );
+
+ expect(hiddenFilesToggle).toBeDefined();
+ fireEvent.click(hiddenFilesToggle!);
+
+ expect(mockOnShowHiddenFilesChange).toHaveBeenCalledWith(true);
+ });
+});
diff --git a/app/src/components/settings/workspace/GeneralSettings.test.tsx b/app/src/components/settings/workspace/GeneralSettings.test.tsx
new file mode 100644
index 0000000..0db590e
--- /dev/null
+++ b/app/src/components/settings/workspace/GeneralSettings.test.tsx
@@ -0,0 +1,61 @@
+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 GeneralSettings from './GeneralSettings';
+
+// 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('GeneralSettings', () => {
+ const mockOnInputChange = vi.fn();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('renders workspace name input with current value', () => {
+ render(
+
+ );
+
+ const nameInput = screen.getByDisplayValue('My Workspace');
+ expect(nameInput).toBeInTheDocument();
+ expect(screen.getByText('Workspace Name')).toBeInTheDocument();
+ });
+
+ it('renders with empty name', () => {
+ render();
+
+ const nameInput = screen.getByPlaceholderText('Enter workspace name');
+ expect(nameInput).toHaveValue('');
+ });
+
+ it('calls onInputChange when name is modified', () => {
+ render(
+
+ );
+
+ const nameInput = screen.getByDisplayValue('Old Name');
+ fireEvent.change(nameInput, { target: { value: 'New Workspace Name' } });
+
+ expect(mockOnInputChange).toHaveBeenCalledWith(
+ 'name',
+ 'New Workspace Name'
+ );
+ });
+
+ it('has required attribute on input', () => {
+ render();
+
+ const nameInput = screen.getByDisplayValue('Test');
+ expect(nameInput).toHaveAttribute('required');
+ });
+});
diff --git a/app/src/components/settings/workspace/GitSettings.test.tsx b/app/src/components/settings/workspace/GitSettings.test.tsx
new file mode 100644
index 0000000..3439917
--- /dev/null
+++ b/app/src/components/settings/workspace/GitSettings.test.tsx
@@ -0,0 +1,134 @@
+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 GitSettings from './GitSettings';
+
+// 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('GitSettings', () => {
+ const mockOnInputChange = vi.fn();
+
+ const defaultProps = {
+ gitEnabled: false,
+ gitUrl: '',
+ gitUser: '',
+ gitToken: '',
+ gitAutoCommit: false,
+ gitCommitMsgTemplate: '',
+ gitCommitName: '',
+ gitCommitEmail: '',
+ onInputChange: mockOnInputChange,
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('renders all git settings fields', () => {
+ render();
+
+ expect(screen.getByText('Enable Git Repository')).toBeInTheDocument();
+ expect(screen.getByText('Git URL')).toBeInTheDocument();
+ expect(screen.getByText('Username')).toBeInTheDocument();
+ expect(screen.getByText('Access Token')).toBeInTheDocument();
+ expect(screen.getByText('Commit on Save')).toBeInTheDocument();
+ expect(screen.getByText('Commit Message Template')).toBeInTheDocument();
+ expect(screen.getByText('Commit Author')).toBeInTheDocument();
+ expect(screen.getByText('Commit Author Email')).toBeInTheDocument();
+ });
+
+ it('disables all inputs when git is not enabled', () => {
+ render();
+
+ expect(screen.getByPlaceholderText('Enter Git URL')).toBeDisabled();
+ expect(screen.getByPlaceholderText('Enter Git username')).toBeDisabled();
+ expect(screen.getByPlaceholderText('Enter Git token')).toBeDisabled();
+
+ const switches = screen.getAllByRole('switch');
+ const commitOnSaveSwitch = switches[1]; // Second switch is commit on save
+ expect(commitOnSaveSwitch).toBeDisabled();
+ });
+
+ it('enables all inputs when git is enabled', () => {
+ render();
+
+ expect(screen.getByPlaceholderText('Enter Git URL')).not.toBeDisabled();
+ expect(
+ screen.getByPlaceholderText('Enter Git username')
+ ).not.toBeDisabled();
+ expect(screen.getByPlaceholderText('Enter Git token')).not.toBeDisabled();
+
+ const switches = screen.getAllByRole('switch');
+ const commitOnSaveSwitch = switches[1];
+ expect(commitOnSaveSwitch).not.toBeDisabled();
+ });
+
+ it('calls onInputChange when git enabled toggle is changed', () => {
+ render();
+
+ const switches = screen.getAllByRole('switch');
+ const gitEnabledSwitch = switches[0];
+ expect(gitEnabledSwitch).toBeDefined();
+
+ fireEvent.click(gitEnabledSwitch!);
+
+ expect(mockOnInputChange).toHaveBeenCalledWith('gitEnabled', true);
+ });
+
+ it('calls onInputChange when git URL is changed', () => {
+ render();
+
+ const urlInput = screen.getByPlaceholderText('Enter Git URL');
+ fireEvent.change(urlInput, {
+ target: { value: 'https://github.com/user/repo.git' },
+ });
+
+ expect(mockOnInputChange).toHaveBeenCalledWith(
+ 'gitUrl',
+ 'https://github.com/user/repo.git'
+ );
+ });
+
+ it('calls onInputChange when commit template is changed', () => {
+ render();
+
+ const templateInput = screen.getByPlaceholderText(
+ 'Enter commit message template'
+ );
+ fireEvent.change(templateInput, {
+ target: { value: '${action}: ${filename}' },
+ });
+
+ expect(mockOnInputChange).toHaveBeenCalledWith(
+ 'gitCommitMsgTemplate',
+ '${action}: ${filename}'
+ );
+ });
+
+ it('shows current values in form fields', () => {
+ const propsWithValues = {
+ ...defaultProps,
+ gitEnabled: true,
+ gitUrl: 'https://github.com/test/repo.git',
+ gitUser: 'testuser',
+ gitCommitMsgTemplate: 'Update ${filename}',
+ };
+
+ render();
+
+ expect(
+ screen.getByDisplayValue('https://github.com/test/repo.git')
+ ).toBeInTheDocument();
+ expect(screen.getByDisplayValue('testuser')).toBeInTheDocument();
+ expect(screen.getByDisplayValue('Update ${filename}')).toBeInTheDocument();
+ });
+});
diff --git a/app/src/components/settings/workspace/WorkspaceSettings.test.tsx b/app/src/components/settings/workspace/WorkspaceSettings.test.tsx
new file mode 100644
index 0000000..a9603db
--- /dev/null
+++ b/app/src/components/settings/workspace/WorkspaceSettings.test.tsx
@@ -0,0 +1,212 @@
+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 WorkspaceSettings from './WorkspaceSettings';
+import { Theme } from '@/types/models';
+
+const mockUpdateSettings = vi.fn();
+vi.mock('../../../hooks/useWorkspace', () => ({
+ useWorkspace: vi.fn(),
+}));
+
+const mockSetSettingsModalVisible = vi.fn();
+vi.mock('../../../contexts/ModalContext', () => ({
+ useModalContext: () => ({
+ settingsModalVisible: true,
+ setSettingsModalVisible: mockSetSettingsModalVisible,
+ }),
+}));
+
+vi.mock('@mantine/notifications', () => ({
+ notifications: {
+ show: vi.fn(),
+ },
+}));
+
+vi.mock('./GeneralSettings', () => ({
+ default: ({
+ name,
+ onInputChange,
+ }: {
+ name: string;
+ onInputChange: (key: string, value: string) => void;
+ }) => (
+
+ onInputChange('name', e.target.value)}
+ />
+
+ ),
+}));
+
+vi.mock('./AppearanceSettings', () => ({
+ default: ({ onThemeChange }: { onThemeChange: (theme: string) => void }) => (
+
+
+
+ ),
+}));
+
+vi.mock('./EditorSettings', () => ({
+ default: () => Editor Settings
,
+}));
+
+vi.mock('./GitSettings', () => ({
+ default: () => Git Settings
,
+}));
+
+vi.mock('./DangerZoneSettings', () => ({
+ default: () => Danger Zone
,
+}));
+
+// 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('WorkspaceSettings', () => {
+ beforeEach(async () => {
+ vi.clearAllMocks();
+ mockUpdateSettings.mockResolvedValue(undefined);
+
+ const { useWorkspace } = await import('../../../hooks/useWorkspace');
+ vi.mocked(useWorkspace).mockReturnValue({
+ currentWorkspace: {
+ name: 'Test Workspace',
+ createdAt: '2024-01-01T00:00:00Z',
+ theme: Theme.Light,
+ autoSave: false,
+ showHiddenFiles: false,
+ gitEnabled: false,
+ gitUrl: '',
+ gitUser: '',
+ gitToken: '',
+ gitAutoCommit: false,
+ gitCommitMsgTemplate: '',
+ gitCommitName: '',
+ gitCommitEmail: '',
+ },
+ workspaces: [],
+ settings: {
+ id: 1,
+ userId: 1,
+ name: 'Test Workspace',
+ createdAt: '2024-01-01T00:00:00Z',
+ theme: Theme.Light,
+ autoSave: false,
+ showHiddenFiles: false,
+ gitEnabled: false,
+ gitUrl: '',
+ gitUser: '',
+ gitToken: '',
+ gitAutoCommit: false,
+ gitCommitMsgTemplate: '',
+ gitCommitName: '',
+ gitCommitEmail: '',
+ },
+ updateSettings: mockUpdateSettings,
+ loading: false,
+ colorScheme: 'light',
+ updateColorScheme: vi.fn(),
+ switchWorkspace: vi.fn(),
+ deleteCurrentWorkspace: vi.fn(),
+ });
+ });
+
+ it('renders modal with all setting sections', () => {
+ render();
+
+ expect(screen.getByText('Workspace Settings')).toBeInTheDocument();
+ expect(screen.getByTestId('general-settings')).toBeInTheDocument();
+ expect(screen.getByTestId('appearance-settings')).toBeInTheDocument();
+ expect(screen.getByTestId('editor-settings')).toBeInTheDocument();
+ expect(screen.getByTestId('git-settings')).toBeInTheDocument();
+ expect(screen.getByTestId('danger-zone-settings')).toBeInTheDocument();
+ });
+
+ it('shows unsaved changes badge when settings are modified', () => {
+ render();
+
+ const nameInput = screen.getByTestId('workspace-name-input');
+ fireEvent.change(nameInput, { target: { value: 'Updated Workspace' } });
+
+ expect(screen.getByText('Unsaved Changes')).toBeInTheDocument();
+ });
+
+ it('saves settings successfully', async () => {
+ render();
+
+ const nameInput = screen.getByTestId('workspace-name-input');
+ fireEvent.change(nameInput, { target: { value: 'Updated Workspace' } });
+
+ const saveButton = screen.getByRole('button', { name: 'Save Changes' });
+ expect(saveButton).toBeDefined();
+ fireEvent.click(saveButton);
+
+ await waitFor(() => {
+ expect(mockUpdateSettings).toHaveBeenCalledWith(
+ expect.objectContaining({ name: 'Updated Workspace' })
+ );
+ });
+
+ await waitFor(() => {
+ expect(mockSetSettingsModalVisible).toHaveBeenCalledWith(false);
+ });
+ });
+
+ it('handles theme changes', () => {
+ render();
+
+ const themeToggle = screen.getByTestId('theme-toggle');
+ fireEvent.click(themeToggle);
+
+ expect(screen.getByText('Unsaved Changes')).toBeInTheDocument();
+ });
+
+ it('closes modal when cancel is clicked', () => {
+ render();
+
+ const cancelButton = screen.getByRole('button', { name: 'Cancel' });
+ fireEvent.click(cancelButton);
+
+ expect(mockSetSettingsModalVisible).toHaveBeenCalledWith(false);
+ });
+
+ it('prevents saving with empty workspace name', async () => {
+ const { notifications } = await import('@mantine/notifications');
+
+ render();
+
+ const nameInput = screen.getByTestId('workspace-name-input');
+ fireEvent.change(nameInput, { target: { value: ' ' } }); // Empty/whitespace
+
+ const saveButton = screen.getByRole('button', { name: 'Save Changes' });
+ fireEvent.click(saveButton);
+
+ await waitFor(() => {
+ expect(notifications.show).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Workspace name cannot be empty',
+ color: 'red',
+ })
+ );
+ });
+
+ expect(mockUpdateSettings).not.toHaveBeenCalled();
+ });
+});
diff --git a/app/src/components/settings/workspace/WorkspaceSettings.tsx b/app/src/components/settings/workspace/WorkspaceSettings.tsx
index f20b01c..109f999 100644
--- a/app/src/components/settings/workspace/WorkspaceSettings.tsx
+++ b/app/src/components/settings/workspace/WorkspaceSettings.tsx
@@ -235,7 +235,7 @@ const WorkspaceSettings: React.FC = () => {
-
+