Add tests for FileActions and FileTree components

This commit is contained in:
2025-07-05 16:54:47 +02:00
parent fffd93afeb
commit 7742a04d9a
3 changed files with 440 additions and 1 deletions

View File

@@ -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 }) => (
<div>{children}</div>
);
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(
<TestWrapper>
<FileActions
handlePullChanges={mockHandlePullChanges}
selectedFile={null}
/>
</TestWrapper>
);
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(
<TestWrapper>
<FileActions
handlePullChanges={mockHandlePullChanges}
selectedFile="test.md"
/>
</TestWrapper>
);
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(
<TestWrapper>
<FileActions
handlePullChanges={mockHandlePullChanges}
selectedFile={null}
/>
</TestWrapper>
);
const deleteButton = getByTestId('delete-file-button');
expect(deleteButton).toBeDisabled();
});
it('calls pull changes when pull button is clicked', () => {
mockHandlePullChanges.mockResolvedValue(true);
const { getByTestId } = render(
<TestWrapper>
<FileActions
handlePullChanges={mockHandlePullChanges}
selectedFile="test.md"
/>
</TestWrapper>
);
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(
<TestWrapper>
<FileActions
handlePullChanges={mockHandlePullChanges}
selectedFile="test.md"
/>
</TestWrapper>
);
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(
<TestWrapper>
<FileActions
handlePullChanges={mockHandlePullChanges}
selectedFile="test.md"
/>
</TestWrapper>
);
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(
<TestWrapper>
<FileActions
handlePullChanges={mockHandlePullChanges}
selectedFile="test.md"
/>
</TestWrapper>
);
const commitButton = getByTestId('commit-push-button');
expect(commitButton).toBeDisabled();
});
});

View File

@@ -32,7 +32,13 @@ const FileActions: React.FC<FileActionsProps> = ({
return ( return (
<Group gap="xs"> <Group gap="xs">
<Tooltip label="Create new file"> <Tooltip label="Create new file">
<ActionIcon variant="default" size="md" onClick={handleCreateFile}> <ActionIcon
variant="default"
size="md"
onClick={handleCreateFile}
aria-label="Create new file"
data-testid="create-file-button"
>
<IconPlus size={16} /> <IconPlus size={16} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
@@ -46,6 +52,8 @@ const FileActions: React.FC<FileActionsProps> = ({
onClick={handleDeleteFile} onClick={handleDeleteFile}
disabled={!selectedFile} disabled={!selectedFile}
color="red" color="red"
aria-label="Delete current file"
data-testid="delete-file-button"
> >
<IconTrash size={16} /> <IconTrash size={16} />
</ActionIcon> </ActionIcon>
@@ -67,6 +75,8 @@ const FileActions: React.FC<FileActionsProps> = ({
}); });
}} }}
disabled={!settings.gitEnabled} disabled={!settings.gitEnabled}
aria-label="Pull changes from remote"
data-testid="pull-changes-button"
> >
<IconGitPullRequest size={16} /> <IconGitPullRequest size={16} />
</ActionIcon> </ActionIcon>
@@ -86,6 +96,8 @@ const FileActions: React.FC<FileActionsProps> = ({
size="md" size="md"
onClick={handleCommitAndPush} onClick={handleCommitAndPush}
disabled={!settings.gitEnabled || settings.gitAutoCommit} disabled={!settings.gitEnabled || settings.gitAutoCommit}
aria-label="Commit and push changes"
data-testid="commit-push-button"
> >
<IconGitCommit size={16} /> <IconGitCommit size={16} />
</ActionIcon> </ActionIcon>

View File

@@ -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<string, unknown>;
onNodeClick: (node: { isInternal: boolean }) => void;
}) => React.ReactNode;
data: FileNode[];
onActivate: (node: { isInternal: boolean; data: FileNode }) => void;
}) => (
<div data-testid="file-tree">
{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 (
<div
key={file.id}
data-testid={`file-node-${file.id}`}
onClick={() => {
// 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 });
}
},
})}
</div>
);
})}
</div>
),
}));
// Mock resize observer hook
vi.mock('@react-hook/resize-observer', () => ({
default: vi.fn(),
}));
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
);
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(
<TestWrapper>
<FileTree
files={mockFiles}
handleFileSelect={mockHandleFileSelect}
showHiddenFiles={true}
/>
</TestWrapper>
);
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(
<TestWrapper>
<FileTree
files={mockFiles}
handleFileSelect={mockHandleFileSelect}
showHiddenFiles={true}
/>
</TestWrapper>
);
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(
<TestWrapper>
<FileTree
files={mockFiles}
handleFileSelect={mockHandleFileSelect}
showHiddenFiles={false}
/>
</TestWrapper>
);
// 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(
<TestWrapper>
<FileTree
files={mockFiles}
handleFileSelect={mockHandleFileSelect}
showHiddenFiles={true}
/>
</TestWrapper>
);
// 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(
<TestWrapper>
<FileTree
files={[]}
handleFileSelect={mockHandleFileSelect}
showHiddenFiles={true}
/>
</TestWrapper>
);
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(
<TestWrapper>
<FileTree
files={mockFiles}
handleFileSelect={mockHandleFileSelect}
showHiddenFiles={true}
/>
</TestWrapper>
);
// 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();
});
});
});