diff --git a/app/src/components/layout/Header.test.tsx b/app/src/components/layout/Header.test.tsx new file mode 100644 index 0000000..a48e1ca --- /dev/null +++ b/app/src/components/layout/Header.test.tsx @@ -0,0 +1,62 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render } from '../../test/utils'; +import Header from './Header'; + +// Mock the child components +vi.mock('../navigation/UserMenu', () => ({ + default: () =>
User Menu
, +})); + +vi.mock('../navigation/WorkspaceSwitcher', () => ({ + default: () =>
Workspace Switcher
, +})); + +vi.mock('../settings/workspace/WorkspaceSettings', () => ({ + default: () =>
Workspace Settings
, +})); + +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( +
{children}
+); + +describe('Header', () => { + it('renders the app title', () => { + const { getByText } = render( + +
+ + ); + + expect(getByText('Lemma')).toBeInTheDocument(); + }); + + it('renders user menu component', () => { + const { getByTestId } = render( + +
+ + ); + + expect(getByTestId('user-menu')).toBeInTheDocument(); + }); + + it('renders workspace switcher component', () => { + const { getByTestId } = render( + +
+ + ); + + expect(getByTestId('workspace-switcher')).toBeInTheDocument(); + }); + + it('renders workspace settings component', () => { + const { getByTestId } = render( + +
+ + ); + + expect(getByTestId('workspace-settings')).toBeInTheDocument(); + }); +}); diff --git a/app/src/components/layout/Layout.test.tsx b/app/src/components/layout/Layout.test.tsx new file mode 100644 index 0000000..7cc0097 --- /dev/null +++ b/app/src/components/layout/Layout.test.tsx @@ -0,0 +1,159 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render } from '../../test/utils'; +import Layout from './Layout'; +import { Theme, type FileNode } from '../../types/models'; + +// Mock child components +vi.mock('./Header', () => ({ + default: () =>
Header
, +})); + +vi.mock('./Sidebar', () => ({ + default: () =>
Sidebar
, +})); + +vi.mock('./MainContent', () => ({ + default: () =>
Main Content
, +})); + +// Mock hooks +vi.mock('../../hooks/useFileNavigation', () => ({ + useFileNavigation: vi.fn(), +})); + +vi.mock('../../hooks/useFileList', () => ({ + useFileList: vi.fn(), +})); + +vi.mock('../../hooks/useWorkspace', () => ({ + useWorkspace: vi.fn(), +})); + +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( +
{children}
+); + +describe('Layout', () => { + const mockHandleFileSelect = vi.fn(); + const mockLoadFileList = vi.fn(); + + const mockCurrentWorkspace = { + id: 1, + name: 'Test Workspace', + createdAt: '2024-01-01T00:00:00Z', + gitEnabled: true, + gitAutoCommit: false, + theme: Theme.Light, + autoSave: true, + showHiddenFiles: false, + gitUrl: '', + gitBranch: 'main', + gitUsername: '', + gitEmail: '', + gitToken: '', + gitUser: '', + gitCommitMsgTemplate: '', + gitCommitName: '', + gitCommitEmail: '', + }; + + const mockFiles: FileNode[] = [ + { + id: '1', + name: 'README.md', + path: 'README.md', + }, + ]; + + beforeEach(async () => { + vi.clearAllMocks(); + + const { useWorkspace } = await import('../../hooks/useWorkspace'); + vi.mocked(useWorkspace).mockReturnValue({ + currentWorkspace: mockCurrentWorkspace, + workspaces: [], + settings: mockCurrentWorkspace, + updateSettings: vi.fn(), + loading: false, + colorScheme: 'light', + updateColorScheme: vi.fn(), + switchWorkspace: vi.fn(), + deleteCurrentWorkspace: vi.fn(), + }); + + const { useFileNavigation } = await import('../../hooks/useFileNavigation'); + vi.mocked(useFileNavigation).mockReturnValue({ + selectedFile: 'README.md', + isNewFile: false, + handleFileSelect: mockHandleFileSelect, + }); + + const { useFileList } = await import('../../hooks/useFileList'); + vi.mocked(useFileList).mockReturnValue({ + files: mockFiles, + loadFileList: mockLoadFileList, + }); + }); + + it('renders all layout components when workspace is loaded', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('header')).toBeInTheDocument(); + expect(getByTestId('sidebar')).toBeInTheDocument(); + expect(getByTestId('main-content')).toBeInTheDocument(); + }); + + it('shows loading spinner when workspace is loading', async () => { + const { useWorkspace } = await import('../../hooks/useWorkspace'); + vi.mocked(useWorkspace).mockReturnValue({ + currentWorkspace: mockCurrentWorkspace, + workspaces: [], + settings: mockCurrentWorkspace, + updateSettings: vi.fn(), + loading: true, + colorScheme: 'light', + updateColorScheme: vi.fn(), + switchWorkspace: vi.fn(), + deleteCurrentWorkspace: vi.fn(), + }); + + const { getByRole } = render( + + + + ); + + expect( + getByRole('status', { name: 'Loading workspace' }) + ).toBeInTheDocument(); + }); + + it('shows no workspace message when no current workspace', async () => { + const { useWorkspace } = await import('../../hooks/useWorkspace'); + vi.mocked(useWorkspace).mockReturnValue({ + currentWorkspace: null, + workspaces: [], + settings: mockCurrentWorkspace, + updateSettings: vi.fn(), + loading: false, + colorScheme: 'light', + updateColorScheme: vi.fn(), + switchWorkspace: vi.fn(), + deleteCurrentWorkspace: vi.fn(), + }); + + const { getByText } = render( + + + + ); + + expect( + getByText('No workspace found. Please create a workspace.') + ).toBeInTheDocument(); + }); +}); diff --git a/app/src/components/layout/Layout.tsx b/app/src/components/layout/Layout.tsx index 98e97b8..670dcfc 100644 --- a/app/src/components/layout/Layout.tsx +++ b/app/src/components/layout/Layout.tsx @@ -14,7 +14,11 @@ const Layout: React.FC = () => { if (workspaceLoading) { return ( -
+
); diff --git a/app/src/components/layout/MainContent.test.tsx b/app/src/components/layout/MainContent.test.tsx new file mode 100644 index 0000000..2972091 --- /dev/null +++ b/app/src/components/layout/MainContent.test.tsx @@ -0,0 +1,159 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render } from '../../test/utils'; +import MainContent from './MainContent'; + +// Mock child components +vi.mock('../editor/ContentView', () => ({ + default: ({ + activeTab, + selectedFile, + }: { + activeTab: string; + selectedFile: string | null; + }) => ( +
+ Content View - {activeTab} - {selectedFile || 'No file'} +
+ ), +})); + +vi.mock('../modals/file/CreateFileModal', () => ({ + default: () =>
Create File Modal
, +})); + +vi.mock('../modals/file/DeleteFileModal', () => ({ + default: () =>
Delete File Modal
, +})); + +vi.mock('../modals/git/CommitMessageModal', () => ({ + default: () => ( +
Commit Message Modal
+ ), +})); + +// Mock hooks +vi.mock('../../hooks/useFileContent', () => ({ + useFileContent: vi.fn(), +})); + +vi.mock('../../hooks/useFileOperations', () => ({ + useFileOperations: vi.fn(), +})); + +vi.mock('../../hooks/useGitOperations', () => ({ + useGitOperations: vi.fn(), +})); + +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( +
{children}
+); + +describe('MainContent', () => { + const mockHandleFileSelect = vi.fn(); + const mockLoadFileList = vi.fn(); + const mockHandleContentChange = vi.fn(); + const mockSetHasUnsavedChanges = vi.fn(); + const mockHandleSave = vi.fn(); + const mockHandleCreate = vi.fn(); + const mockHandleDelete = vi.fn(); + const mockHandleCommitAndPush = vi.fn(); + + beforeEach(async () => { + vi.clearAllMocks(); + + const { useFileContent } = await import('../../hooks/useFileContent'); + vi.mocked(useFileContent).mockReturnValue({ + content: 'Test content', + setContent: vi.fn(), + hasUnsavedChanges: false, + setHasUnsavedChanges: mockSetHasUnsavedChanges, + loadFileContent: vi.fn(), + handleContentChange: mockHandleContentChange, + }); + + const { useFileOperations } = await import('../../hooks/useFileOperations'); + vi.mocked(useFileOperations).mockReturnValue({ + handleSave: mockHandleSave, + handleCreate: mockHandleCreate, + handleDelete: mockHandleDelete, + }); + + const { useGitOperations } = await import('../../hooks/useGitOperations'); + vi.mocked(useGitOperations).mockReturnValue({ + handlePull: vi.fn(), + handleCommitAndPush: mockHandleCommitAndPush, + }); + }); + + it('shows breadcrumbs for selected file', () => { + const { getByText } = render( + + + + ); + + expect(getByText('docs')).toBeInTheDocument(); + expect(getByText('guide.md')).toBeInTheDocument(); + }); + + it('shows unsaved changes indicator when file has changes', async () => { + const { useFileContent } = await import('../../hooks/useFileContent'); + vi.mocked(useFileContent).mockReturnValue({ + content: 'Test content', + setContent: vi.fn(), + hasUnsavedChanges: true, + setHasUnsavedChanges: mockSetHasUnsavedChanges, + loadFileContent: vi.fn(), + handleContentChange: mockHandleContentChange, + }); + + const { container } = render( + + + + ); + + // Should show unsaved changes indicator (yellow dot) + const indicator = container.querySelector('svg[style*="yellow"]'); + expect(indicator).toBeInTheDocument(); + }); + + it('renders all modal components', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('create-file-modal')).toBeInTheDocument(); + expect(getByTestId('delete-file-modal')).toBeInTheDocument(); + expect(getByTestId('commit-message-modal')).toBeInTheDocument(); + }); + + it('handles no selected file', () => { + const { getByTestId } = render( + + + + ); + + const contentView = getByTestId('content-view'); + expect(contentView).toHaveTextContent('Content View - source - No file'); + }); +}); diff --git a/app/src/components/layout/Sidebar.test.tsx b/app/src/components/layout/Sidebar.test.tsx new file mode 100644 index 0000000..608b499 --- /dev/null +++ b/app/src/components/layout/Sidebar.test.tsx @@ -0,0 +1,181 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render } from '../../test/utils'; +import Sidebar from './Sidebar'; +import { Theme, type FileNode } from '../../types/models'; + +// Mock the child components +vi.mock('../files/FileActions', () => ({ + default: ({ selectedFile }: { selectedFile: string | null }) => ( +
+ File Actions - {selectedFile || 'No file'} +
+ ), +})); + +vi.mock('../files/FileTree', () => ({ + default: ({ + files, + showHiddenFiles, + }: { + files: FileNode[]; + showHiddenFiles: boolean; + }) => ( +
+ File Tree - {files.length} files - Hidden: {showHiddenFiles.toString()} +
+ ), +})); + +// Mock the hooks +vi.mock('../../hooks/useGitOperations', () => ({ + useGitOperations: vi.fn(), +})); + +vi.mock('../../hooks/useWorkspace', () => ({ + useWorkspace: vi.fn(), +})); + +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( +
{children}
+); + +describe('Sidebar', () => { + const mockHandleFileSelect = vi.fn(); + const mockLoadFileList = vi.fn(); + const mockHandlePull = vi.fn(); + + const mockFiles: FileNode[] = [ + { + id: '1', + name: 'README.md', + path: 'README.md', + }, + { + id: '2', + name: 'docs', + path: 'docs', + children: [], + }, + ]; + + const mockSettings = { + gitEnabled: true, + gitAutoCommit: false, + theme: Theme.Light, + autoSave: true, + showHiddenFiles: false, + gitUrl: '', + gitBranch: 'main', + gitUsername: '', + gitEmail: '', + gitToken: '', + gitUser: '', + gitCommitMsgTemplate: '', + gitCommitName: '', + gitCommitEmail: '', + }; + + beforeEach(async () => { + vi.clearAllMocks(); + + const { useGitOperations } = await import('../../hooks/useGitOperations'); + vi.mocked(useGitOperations).mockReturnValue({ + handlePull: mockHandlePull, + handleCommitAndPush: vi.fn(), + }); + + const { useWorkspace } = await import('../../hooks/useWorkspace'); + vi.mocked(useWorkspace).mockReturnValue({ + currentWorkspace: null, + workspaces: [], + settings: mockSettings, + updateSettings: vi.fn(), + loading: false, + colorScheme: 'light', + updateColorScheme: vi.fn(), + switchWorkspace: vi.fn(), + deleteCurrentWorkspace: vi.fn(), + }); + }); + + it('renders child components', () => { + const { getByTestId } = render( + + + + ); + + const fileActions = getByTestId('file-actions'); + expect(fileActions).toBeInTheDocument(); + expect(fileActions).toHaveTextContent('File Actions - test.md'); + + const fileTree = getByTestId('file-tree'); + expect(fileTree).toBeInTheDocument(); + expect(fileTree).toHaveTextContent('File Tree - 2 files - Hidden: false'); + }); + + it('passes showHiddenFiles setting to file tree', async () => { + const { useWorkspace } = await import('../../hooks/useWorkspace'); + vi.mocked(useWorkspace).mockReturnValue({ + currentWorkspace: null, + workspaces: [], + settings: { ...mockSettings, showHiddenFiles: true }, + updateSettings: vi.fn(), + loading: false, + colorScheme: 'light', + updateColorScheme: vi.fn(), + switchWorkspace: vi.fn(), + deleteCurrentWorkspace: vi.fn(), + }); + + const { getByTestId } = render( + + + + ); + + const fileTree = getByTestId('file-tree'); + expect(fileTree).toHaveTextContent('File Tree - 2 files - Hidden: true'); + }); + + it('shows no file selected when selectedFile is null', () => { + const { getByTestId } = render( + + + + ); + + const fileActions = getByTestId('file-actions'); + expect(fileActions).toHaveTextContent('File Actions - No file'); + }); + + it('calls loadFileList on mount', () => { + render( + + + + ); + + expect(mockLoadFileList).toHaveBeenCalledOnce(); + }); +});