diff --git a/app/src/components/files/FileActions.test.tsx b/app/src/components/files/FileActions.test.tsx new file mode 100644 index 0000000..95a6f76 --- /dev/null +++ b/app/src/components/files/FileActions.test.tsx @@ -0,0 +1,212 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { fireEvent } from '@testing-library/react'; +import { render } from '../../test/utils'; +import FileActions from './FileActions'; +import { Theme } from '@/types/models'; + +// Mock the contexts and hooks +vi.mock('../../contexts/ModalContext', () => ({ + useModalContext: vi.fn(), +})); + +vi.mock('../../hooks/useWorkspace', () => ({ + useWorkspace: vi.fn(), +})); + +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( +
{children}
+); + +describe('FileActions', () => { + const mockHandlePullChanges = vi.fn(); + const mockSetNewFileModalVisible = vi.fn(); + const mockSetDeleteFileModalVisible = vi.fn(); + const mockSetCommitMessageModalVisible = vi.fn(); + + 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 { useModalContext } = await import('../../contexts/ModalContext'); + vi.mocked(useModalContext).mockReturnValue({ + newFileModalVisible: false, + setNewFileModalVisible: mockSetNewFileModalVisible, + deleteFileModalVisible: false, + setDeleteFileModalVisible: mockSetDeleteFileModalVisible, + commitMessageModalVisible: false, + setCommitMessageModalVisible: mockSetCommitMessageModalVisible, + settingsModalVisible: false, + setSettingsModalVisible: vi.fn(), + switchWorkspaceModalVisible: false, + setSwitchWorkspaceModalVisible: vi.fn(), + createWorkspaceModalVisible: false, + setCreateWorkspaceModalVisible: 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('opens new file modal when create button is clicked', () => { + const { getByTestId } = render( + + + + ); + + const createButton = getByTestId('create-file-button'); + fireEvent.click(createButton); + + expect(mockSetNewFileModalVisible).toHaveBeenCalledWith(true); + }); + + it('opens delete modal when delete button is clicked with selected file', () => { + const { getByTestId } = render( + + + + ); + + const deleteButton = getByTestId('delete-file-button'); + fireEvent.click(deleteButton); + + expect(mockSetDeleteFileModalVisible).toHaveBeenCalledWith(true); + }); + + it('disables delete button when no file is selected', () => { + const { getByTestId } = render( + + + + ); + + const deleteButton = getByTestId('delete-file-button'); + expect(deleteButton).toBeDisabled(); + }); + + it('calls pull changes when pull button is clicked', () => { + mockHandlePullChanges.mockResolvedValue(true); + + const { getByTestId } = render( + + + + ); + + const pullButton = getByTestId('pull-changes-button'); + fireEvent.click(pullButton); + + expect(mockHandlePullChanges).toHaveBeenCalledOnce(); + }); + + it('disables git buttons when git is not enabled', async () => { + const { useWorkspace } = await import('../../hooks/useWorkspace'); + vi.mocked(useWorkspace).mockReturnValue({ + currentWorkspace: null, + workspaces: [], + settings: { ...mockSettings, gitEnabled: false }, + updateSettings: vi.fn(), + loading: false, + colorScheme: 'light', + updateColorScheme: vi.fn(), + switchWorkspace: vi.fn(), + deleteCurrentWorkspace: vi.fn(), + }); + + const { getByTestId } = render( + + + + ); + + const pullButton = getByTestId('pull-changes-button'); + expect(pullButton).toBeDisabled(); + + const commitButton = getByTestId('commit-push-button'); + expect(commitButton).toBeDisabled(); + }); + + it('opens commit modal when commit button is clicked', () => { + const { getByTestId } = render( + + + + ); + + const commitButton = getByTestId('commit-push-button'); + fireEvent.click(commitButton); + + expect(mockSetCommitMessageModalVisible).toHaveBeenCalledWith(true); + }); + + it('disables commit button when auto-commit is enabled', async () => { + const { useWorkspace } = await import('../../hooks/useWorkspace'); + vi.mocked(useWorkspace).mockReturnValue({ + currentWorkspace: null, + workspaces: [], + settings: { ...mockSettings, gitAutoCommit: true }, + updateSettings: vi.fn(), + loading: false, + colorScheme: 'light', + updateColorScheme: vi.fn(), + switchWorkspace: vi.fn(), + deleteCurrentWorkspace: vi.fn(), + }); + + const { getByTestId } = render( + + + + ); + + const commitButton = getByTestId('commit-push-button'); + expect(commitButton).toBeDisabled(); + }); +}); diff --git a/app/src/components/files/FileActions.tsx b/app/src/components/files/FileActions.tsx index 51ff89a..2695c79 100644 --- a/app/src/components/files/FileActions.tsx +++ b/app/src/components/files/FileActions.tsx @@ -32,7 +32,13 @@ const FileActions: React.FC = ({ return ( - + @@ -46,6 +52,8 @@ const FileActions: React.FC = ({ onClick={handleDeleteFile} disabled={!selectedFile} color="red" + aria-label="Delete current file" + data-testid="delete-file-button" > @@ -67,6 +75,8 @@ const FileActions: React.FC = ({ }); }} disabled={!settings.gitEnabled} + aria-label="Pull changes from remote" + data-testid="pull-changes-button" > @@ -86,6 +96,8 @@ const FileActions: React.FC = ({ size="md" onClick={handleCommitAndPush} disabled={!settings.gitEnabled || settings.gitAutoCommit} + aria-label="Commit and push changes" + data-testid="commit-push-button" > diff --git a/app/src/components/files/FileTree.test.tsx b/app/src/components/files/FileTree.test.tsx new file mode 100644 index 0000000..09ec508 --- /dev/null +++ b/app/src/components/files/FileTree.test.tsx @@ -0,0 +1,215 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { render } from '../../test/utils'; +import FileTree from './FileTree'; +import type { FileNode } from '../../types/models'; + +// Mock react-arborist +vi.mock('react-arborist', () => ({ + Tree: ({ + children, + data, + onActivate, + }: { + children: (props: { + node: { + data: FileNode; + isLeaf: boolean; + isInternal: boolean; + isOpen: boolean; + level: number; + toggle: () => void; + }; + style: Record; + onNodeClick: (node: { isInternal: boolean }) => void; + }) => React.ReactNode; + data: FileNode[]; + onActivate: (node: { isInternal: boolean; data: FileNode }) => void; + }) => ( +
+ {data.map((file) => { + const mockNode = { + data: file, + isLeaf: !file.children || file.children.length === 0, + isInternal: !!(file.children && file.children.length > 0), + isOpen: false, + level: 0, + toggle: vi.fn(), + }; + + return ( +
{ + // Simulate the Tree's onActivate behavior + if (!mockNode.isInternal) { + onActivate({ isInternal: mockNode.isInternal, data: file }); + } + }} + > + {children({ + node: mockNode, + style: {}, + onNodeClick: (node: { isInternal: boolean }) => { + if (!node.isInternal) { + onActivate({ isInternal: node.isInternal, data: file }); + } + }, + })} +
+ ); + })} +
+ ), +})); + +// Mock resize observer hook +vi.mock('@react-hook/resize-observer', () => ({ + default: vi.fn(), +})); + +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( +
{children}
+); + +describe('FileTree', () => { + const mockHandleFileSelect = vi.fn(); + + const mockFiles: FileNode[] = [ + { + id: '1', + name: 'README.md', + path: 'README.md', + }, + { + id: '2', + name: 'docs', + path: 'docs', + children: [ + { + id: '3', + name: 'guide.md', + path: 'docs/guide.md', + }, + ], + }, + { + id: '4', + name: '.hidden', + path: '.hidden', + }, + ]; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders file tree with files', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('file-tree')).toBeInTheDocument(); + expect(getByTestId('file-node-1')).toBeInTheDocument(); + expect(getByTestId('file-node-2')).toBeInTheDocument(); + }); + + it('calls handleFileSelect when file is clicked', async () => { + const { getByTestId } = render( + + + + ); + + const fileNode = getByTestId('file-node-1'); + fireEvent.click(fileNode); + + await waitFor(() => { + expect(mockHandleFileSelect).toHaveBeenCalledWith('README.md'); + }); + }); + + it('filters out hidden files when showHiddenFiles is false', () => { + const { getByTestId, queryByTestId } = render( + + + + ); + + // Should show regular files + expect(getByTestId('file-node-1')).toBeInTheDocument(); + expect(getByTestId('file-node-2')).toBeInTheDocument(); + + // Should not show hidden file + expect(queryByTestId('file-node-4')).not.toBeInTheDocument(); + }); + + it('shows hidden files when showHiddenFiles is true', () => { + const { getByTestId } = render( + + + + ); + + // Should show all files including hidden + expect(getByTestId('file-node-1')).toBeInTheDocument(); + expect(getByTestId('file-node-2')).toBeInTheDocument(); + expect(getByTestId('file-node-4')).toBeInTheDocument(); + }); + + it('renders empty tree when no files provided', () => { + const { getByTestId } = render( + + + + ); + + const tree = getByTestId('file-tree'); + expect(tree).toBeInTheDocument(); + expect(tree.children).toHaveLength(0); + }); + + it('does not call handleFileSelect for folder clicks', async () => { + const { getByTestId } = render( + + + + ); + + // Click on folder (has children) + const folderNode = getByTestId('file-node-2'); + fireEvent.click(folderNode); + + // Should not call handleFileSelect for folders + await waitFor(() => { + expect(mockHandleFileSelect).not.toHaveBeenCalled(); + }); + }); +});