mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 23:44:22 +00:00
Add tests for FileActions and FileTree components
This commit is contained in:
212
app/src/components/files/FileActions.test.tsx
Normal file
212
app/src/components/files/FileActions.test.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
|||||||
215
app/src/components/files/FileTree.test.tsx
Normal file
215
app/src/components/files/FileTree.test.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user