mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
1261 lines
43 KiB
TypeScript
1261 lines
43 KiB
TypeScript
import React from 'react';
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
|
|
import { FileExplorerPanel } from '../../../renderer/components/FileExplorerPanel';
|
|
import type { Session, Theme } from '../../../renderer/types';
|
|
|
|
// Mock lucide-react
|
|
vi.mock('lucide-react', () => ({
|
|
ChevronRight: ({ className, style }: { className?: string; style?: React.CSSProperties }) =>
|
|
<span data-testid="chevron-right" className={className} style={style}>▶</span>,
|
|
ChevronDown: ({ className, style }: { className?: string; style?: React.CSSProperties }) =>
|
|
<span data-testid="chevron-down" className={className} style={style}>▼</span>,
|
|
ChevronUp: ({ className, style }: { className?: string; style?: React.CSSProperties }) =>
|
|
<span data-testid="chevron-up" className={className} style={style}>▲</span>,
|
|
Folder: ({ className, style }: { className?: string; style?: React.CSSProperties }) =>
|
|
<span data-testid="folder-icon" className={className} style={style}>📁</span>,
|
|
RefreshCw: ({ className, style }: { className?: string; style?: React.CSSProperties }) =>
|
|
<span data-testid="refresh-icon" className={className} style={style}>🔄</span>,
|
|
Check: ({ className, style }: { className?: string; style?: React.CSSProperties }) =>
|
|
<span data-testid="check-icon" className={className} style={style}>✓</span>,
|
|
}));
|
|
|
|
// Mock createPortal
|
|
vi.mock('react-dom', async () => {
|
|
const actual = await vi.importActual('react-dom');
|
|
return {
|
|
...actual,
|
|
createPortal: (children: React.ReactNode) => children,
|
|
};
|
|
});
|
|
|
|
// Mock LayerStackContext
|
|
const mockRegisterLayer = vi.fn(() => 'layer-123');
|
|
const mockUnregisterLayer = vi.fn();
|
|
const mockUpdateLayerHandler = vi.fn();
|
|
|
|
vi.mock('../../../renderer/contexts/LayerStackContext', () => ({
|
|
useLayerStack: () => ({
|
|
registerLayer: mockRegisterLayer,
|
|
unregisterLayer: mockUnregisterLayer,
|
|
updateLayerHandler: mockUpdateLayerHandler,
|
|
}),
|
|
}));
|
|
|
|
// Mock getFileIcon
|
|
vi.mock('../../../renderer/utils/theme', () => ({
|
|
getFileIcon: (type: string | undefined, theme: Theme) => {
|
|
if (type === 'added') return <span data-testid="added-icon">+</span>;
|
|
if (type === 'modified') return <span data-testid="modified-icon">~</span>;
|
|
if (type === 'deleted') return <span data-testid="deleted-icon">-</span>;
|
|
return <span data-testid="file-icon">📄</span>;
|
|
},
|
|
}));
|
|
|
|
// Mock MODAL_PRIORITIES
|
|
vi.mock('../../../renderer/constants/modalPriorities', () => ({
|
|
MODAL_PRIORITIES: {
|
|
FILE_TREE_FILTER: 50,
|
|
},
|
|
}));
|
|
|
|
// Create mock theme
|
|
const mockTheme: Theme = {
|
|
id: 'test-theme',
|
|
name: 'Test Theme',
|
|
mode: 'dark',
|
|
colors: {
|
|
bgMain: '#1a1a1a',
|
|
bgSidebar: '#2d2d2d',
|
|
bgActivity: '#3d3d3d',
|
|
bgInput: '#404040',
|
|
textMain: '#ffffff',
|
|
textDim: '#888888',
|
|
accent: '#4a9eff',
|
|
border: '#404040',
|
|
success: '#4caf50',
|
|
warning: '#ff9800',
|
|
error: '#f44336',
|
|
info: '#2196f3',
|
|
scrollbarThumb: '#666666',
|
|
},
|
|
};
|
|
|
|
// Create mock session
|
|
const createMockSession = (overrides: Partial<Session> = {}): Session => ({
|
|
id: 'session-1',
|
|
name: 'Test Session',
|
|
toolType: 'claude-code',
|
|
state: 'idle',
|
|
inputMode: 'ai',
|
|
cwd: '/Users/test/project',
|
|
projectRoot: '/Users/test/project',
|
|
fullPath: '/Users/test/project',
|
|
aiPid: 1234,
|
|
terminalPid: 5678,
|
|
aiLogs: [],
|
|
shellLogs: [],
|
|
isGitRepo: true,
|
|
fileTree: [],
|
|
fileExplorerExpanded: [],
|
|
messageQueue: [],
|
|
changedFiles: [],
|
|
...overrides,
|
|
});
|
|
|
|
// Create mock file tree
|
|
const mockFileTree = [
|
|
{
|
|
name: 'src',
|
|
type: 'folder' as const,
|
|
children: [
|
|
{
|
|
name: 'index.ts',
|
|
type: 'file' as const,
|
|
},
|
|
{
|
|
name: 'utils',
|
|
type: 'folder' as const,
|
|
children: [
|
|
{
|
|
name: 'helpers.ts',
|
|
type: 'file' as const,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'package.json',
|
|
type: 'file' as const,
|
|
},
|
|
];
|
|
|
|
describe('FileExplorerPanel', () => {
|
|
let defaultProps: React.ComponentProps<typeof FileExplorerPanel>;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.useFakeTimers();
|
|
|
|
defaultProps = {
|
|
session: createMockSession(),
|
|
theme: mockTheme,
|
|
fileTreeFilter: '',
|
|
setFileTreeFilter: vi.fn(),
|
|
fileTreeFilterOpen: false,
|
|
setFileTreeFilterOpen: vi.fn(),
|
|
filteredFileTree: mockFileTree,
|
|
selectedFileIndex: 0,
|
|
setSelectedFileIndex: vi.fn(),
|
|
activeFocus: 'main',
|
|
activeRightTab: 'files',
|
|
previewFile: null,
|
|
setActiveFocus: vi.fn(),
|
|
fileTreeContainerRef: React.createRef<HTMLDivElement>(),
|
|
fileTreeFilterInputRef: React.createRef<HTMLInputElement>(),
|
|
toggleFolder: vi.fn(),
|
|
handleFileClick: vi.fn().mockResolvedValue(undefined),
|
|
expandAllFolders: vi.fn(),
|
|
collapseAllFolders: vi.fn(),
|
|
updateSessionWorkingDirectory: vi.fn().mockResolvedValue(undefined),
|
|
refreshFileTree: vi.fn().mockResolvedValue({ totalChanges: 0 }),
|
|
setSessions: vi.fn(),
|
|
onAutoRefreshChange: vi.fn(),
|
|
onShowFlash: vi.fn(),
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe('Initial Render', () => {
|
|
it('renders without crashing', () => {
|
|
const { container } = render(<FileExplorerPanel {...defaultProps} />);
|
|
expect(container).toBeTruthy();
|
|
});
|
|
|
|
it('displays the current working directory in header', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
expect(screen.getByTitle('/Users/test/project')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays file tree content', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
expect(screen.getByText('src')).toBeInTheDocument();
|
|
expect(screen.getByText('package.json')).toBeInTheDocument();
|
|
});
|
|
|
|
it('applies theme background color to header', () => {
|
|
const { container } = render(<FileExplorerPanel {...defaultProps} />);
|
|
const header = container.querySelector('.sticky');
|
|
expect(header).toHaveStyle({ backgroundColor: mockTheme.colors.bgSidebar });
|
|
});
|
|
});
|
|
|
|
describe('File Tree Filter', () => {
|
|
it('does not show filter input when closed', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
expect(screen.queryByPlaceholderText('Filter files...')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('shows filter input when fileTreeFilterOpen is true', () => {
|
|
render(<FileExplorerPanel {...defaultProps} fileTreeFilterOpen={true} />);
|
|
expect(screen.getByPlaceholderText('Filter files...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('filter input has autoFocus', () => {
|
|
render(<FileExplorerPanel {...defaultProps} fileTreeFilterOpen={true} />);
|
|
const input = screen.getByPlaceholderText('Filter files...');
|
|
// autoFocus is a React prop that becomes autofocus attribute in HTML
|
|
expect(input).toHaveFocus();
|
|
});
|
|
|
|
it('displays current filter value', () => {
|
|
render(
|
|
<FileExplorerPanel
|
|
{...defaultProps}
|
|
fileTreeFilterOpen={true}
|
|
fileTreeFilter="test"
|
|
/>
|
|
);
|
|
expect(screen.getByDisplayValue('test')).toBeInTheDocument();
|
|
});
|
|
|
|
it('calls setFileTreeFilter on input change', () => {
|
|
render(<FileExplorerPanel {...defaultProps} fileTreeFilterOpen={true} />);
|
|
const input = screen.getByPlaceholderText('Filter files...');
|
|
fireEvent.change(input, { target: { value: 'search' } });
|
|
expect(defaultProps.setFileTreeFilter).toHaveBeenCalledWith('search');
|
|
});
|
|
|
|
it('registers layer when filter is open', () => {
|
|
render(<FileExplorerPanel {...defaultProps} fileTreeFilterOpen={true} />);
|
|
expect(mockRegisterLayer).toHaveBeenCalledWith(expect.objectContaining({
|
|
type: 'overlay',
|
|
priority: 50,
|
|
blocksLowerLayers: false,
|
|
capturesFocus: true,
|
|
focusTrap: 'none',
|
|
allowClickOutside: true,
|
|
ariaLabel: 'File Tree Filter',
|
|
}));
|
|
});
|
|
|
|
it('unregisters layer when filter is closed', () => {
|
|
const { rerender } = render(
|
|
<FileExplorerPanel {...defaultProps} fileTreeFilterOpen={true} />
|
|
);
|
|
expect(mockRegisterLayer).toHaveBeenCalled();
|
|
|
|
rerender(<FileExplorerPanel {...defaultProps} fileTreeFilterOpen={false} />);
|
|
expect(mockUnregisterLayer).toHaveBeenCalledWith('layer-123');
|
|
});
|
|
|
|
it('updates layer handler when filter dependencies change', () => {
|
|
render(<FileExplorerPanel {...defaultProps} fileTreeFilterOpen={true} />);
|
|
expect(mockUpdateLayerHandler).toHaveBeenCalled();
|
|
});
|
|
|
|
it('shows no results message when filter has no matches', () => {
|
|
render(
|
|
<FileExplorerPanel
|
|
{...defaultProps}
|
|
fileTreeFilter="nonexistent"
|
|
filteredFileTree={[]}
|
|
/>
|
|
);
|
|
expect(screen.getByText('No files match your search')).toBeInTheDocument();
|
|
});
|
|
|
|
it('applies accent color border to filter input', () => {
|
|
render(<FileExplorerPanel {...defaultProps} fileTreeFilterOpen={true} />);
|
|
const input = screen.getByPlaceholderText('Filter files...');
|
|
expect(input).toHaveStyle({ borderColor: mockTheme.colors.accent });
|
|
});
|
|
});
|
|
|
|
describe('Header Controls', () => {
|
|
it('renders refresh button', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
expect(screen.getByTestId('refresh-icon')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders expand all button with correct title', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
expect(screen.getByTitle('Expand all folders')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders collapse all button with correct title', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
expect(screen.getByTitle('Collapse all folders')).toBeInTheDocument();
|
|
});
|
|
|
|
it('calls expandAllFolders when expand button is clicked', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const expandButton = screen.getByTitle('Expand all folders');
|
|
fireEvent.click(expandButton);
|
|
expect(defaultProps.expandAllFolders).toHaveBeenCalledWith(
|
|
'session-1',
|
|
expect.any(Object),
|
|
expect.any(Function)
|
|
);
|
|
});
|
|
|
|
it('calls collapseAllFolders when collapse button is clicked', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const collapseButton = screen.getByTitle('Collapse all folders');
|
|
fireEvent.click(collapseButton);
|
|
expect(defaultProps.collapseAllFolders).toHaveBeenCalledWith(
|
|
'session-1',
|
|
expect.any(Function)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Refresh Button', () => {
|
|
it('shows default title when no auto-refresh', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
expect(screen.getByTitle('Refresh file tree')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows auto-refresh title when interval is set', () => {
|
|
const session = createMockSession({ fileTreeAutoRefreshInterval: 20 });
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
expect(screen.getByTitle('Auto-refresh every 20s')).toBeInTheDocument();
|
|
});
|
|
|
|
it('applies accent color when auto-refresh is active', () => {
|
|
const session = createMockSession({ fileTreeAutoRefreshInterval: 20 });
|
|
const { container } = render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
const refreshButton = container.querySelector('[title="Auto-refresh every 20s"]');
|
|
expect(refreshButton).toHaveStyle({ color: mockTheme.colors.accent });
|
|
});
|
|
|
|
it('calls refreshFileTree when clicked', async () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
fireEvent.click(refreshButton);
|
|
expect(defaultProps.refreshFileTree).toHaveBeenCalledWith('session-1');
|
|
});
|
|
|
|
it('shows flash notification on refresh with 0 changes', async () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
await act(async () => {
|
|
fireEvent.click(refreshButton);
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
});
|
|
|
|
expect(defaultProps.onShowFlash).toHaveBeenCalledWith('No changes detected');
|
|
});
|
|
|
|
it('shows flash notification with change count on refresh', async () => {
|
|
(defaultProps.refreshFileTree as ReturnType<typeof vi.fn>).mockResolvedValue({ totalChanges: 5 });
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
await act(async () => {
|
|
fireEvent.click(refreshButton);
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
});
|
|
|
|
expect(defaultProps.onShowFlash).toHaveBeenCalledWith('Detected 5 changes');
|
|
});
|
|
|
|
it('shows singular form for 1 change', async () => {
|
|
(defaultProps.refreshFileTree as ReturnType<typeof vi.fn>).mockResolvedValue({ totalChanges: 1 });
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
await act(async () => {
|
|
fireEvent.click(refreshButton);
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
});
|
|
|
|
expect(defaultProps.onShowFlash).toHaveBeenCalledWith('Detected 1 change');
|
|
});
|
|
|
|
it('adds spin animation class when refreshing', async () => {
|
|
let resolveRefresh: (value: any) => void;
|
|
(defaultProps.refreshFileTree as ReturnType<typeof vi.fn>).mockImplementation(
|
|
() => new Promise(resolve => { resolveRefresh = resolve; })
|
|
);
|
|
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
await act(async () => {
|
|
fireEvent.click(refreshButton);
|
|
});
|
|
|
|
// During refresh, icon should spin
|
|
const refreshIcon = screen.getByTestId('refresh-icon');
|
|
expect(refreshIcon.className).toContain('animate-spin');
|
|
|
|
// Resolve and wait for animation timeout
|
|
await act(async () => {
|
|
resolveRefresh!({ totalChanges: 0 });
|
|
await vi.advanceTimersByTimeAsync(500);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Auto-refresh Overlay', () => {
|
|
it('shows overlay on hover after delay', async () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
|
|
// Overlay not visible yet
|
|
expect(screen.queryByText('Auto-refresh')).not.toBeInTheDocument();
|
|
|
|
// Wait for hover delay
|
|
act(() => {
|
|
vi.advanceTimersByTime(400);
|
|
});
|
|
|
|
expect(screen.getByText('Auto-refresh')).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not show overlay if mouse leaves before delay', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(200);
|
|
});
|
|
|
|
fireEvent.mouseLeave(refreshButton);
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(200);
|
|
});
|
|
|
|
expect(screen.queryByText('Auto-refresh')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('displays all auto-refresh options', async () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
act(() => {
|
|
vi.advanceTimersByTime(400);
|
|
});
|
|
|
|
expect(screen.getByText('Every 5 seconds')).toBeInTheDocument();
|
|
expect(screen.getByText('Every 20 seconds')).toBeInTheDocument();
|
|
expect(screen.getByText('Every 60 seconds')).toBeInTheDocument();
|
|
expect(screen.getByText('Every 3 minutes')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows check icon for currently selected interval', async () => {
|
|
const session = createMockSession({ fileTreeAutoRefreshInterval: 20 });
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
const refreshButton = screen.getByTitle('Auto-refresh every 20s');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
act(() => {
|
|
vi.advanceTimersByTime(400);
|
|
});
|
|
|
|
expect(screen.getByTestId('check-icon')).toBeInTheDocument();
|
|
});
|
|
|
|
it('calls onAutoRefreshChange when option is selected', async () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
act(() => {
|
|
vi.advanceTimersByTime(400);
|
|
});
|
|
|
|
const option = screen.getByText('Every 5 seconds');
|
|
fireEvent.click(option);
|
|
|
|
expect(defaultProps.onAutoRefreshChange).toHaveBeenCalledWith(5);
|
|
});
|
|
|
|
it('closes overlay after selecting option', async () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
act(() => {
|
|
vi.advanceTimersByTime(400);
|
|
});
|
|
|
|
const option = screen.getByText('Every 5 seconds');
|
|
fireEvent.click(option);
|
|
|
|
// Overlay should close after selection
|
|
expect(screen.queryByText('Every 20 seconds')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('shows disable option when auto-refresh is active', async () => {
|
|
const session = createMockSession({ fileTreeAutoRefreshInterval: 20 });
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
const refreshButton = screen.getByTitle('Auto-refresh every 20s');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
act(() => {
|
|
vi.advanceTimersByTime(400);
|
|
});
|
|
|
|
expect(screen.getByText('Disable auto-refresh')).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not show disable option when auto-refresh is inactive', async () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
act(() => {
|
|
vi.advanceTimersByTime(400);
|
|
});
|
|
|
|
expect(screen.queryByText('Disable auto-refresh')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('calls onAutoRefreshChange with 0 to disable', async () => {
|
|
const session = createMockSession({ fileTreeAutoRefreshInterval: 20 });
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
const refreshButton = screen.getByTitle('Auto-refresh every 20s');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
act(() => {
|
|
vi.advanceTimersByTime(400);
|
|
});
|
|
|
|
const disableOption = screen.getByText('Disable auto-refresh');
|
|
fireEvent.click(disableOption);
|
|
|
|
expect(defaultProps.onAutoRefreshChange).toHaveBeenCalledWith(0);
|
|
});
|
|
|
|
it('keeps overlay open when mouse enters overlay', async () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
act(() => {
|
|
vi.advanceTimersByTime(400);
|
|
});
|
|
|
|
// Leave button
|
|
fireEvent.mouseLeave(refreshButton);
|
|
|
|
// Enter overlay before close delay
|
|
const overlay = screen.getByText('Auto-refresh').closest('div');
|
|
fireEvent.mouseEnter(overlay!);
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(200);
|
|
});
|
|
|
|
// Overlay should still be visible
|
|
expect(screen.getByText('Auto-refresh')).toBeInTheDocument();
|
|
});
|
|
|
|
it('closes overlay when mouse leaves overlay', async () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
act(() => {
|
|
vi.advanceTimersByTime(400);
|
|
});
|
|
|
|
const overlay = screen.getByText('Auto-refresh').closest('.fixed');
|
|
fireEvent.mouseEnter(overlay!);
|
|
fireEvent.mouseLeave(overlay!);
|
|
|
|
expect(screen.queryByText('Auto-refresh')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Auto-refresh Timer', () => {
|
|
it('starts timer when interval is set', () => {
|
|
const session = createMockSession({ fileTreeAutoRefreshInterval: 5 });
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
expect(defaultProps.refreshFileTree).not.toHaveBeenCalled();
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(5000);
|
|
});
|
|
|
|
expect(defaultProps.refreshFileTree).toHaveBeenCalledWith('session-1');
|
|
});
|
|
|
|
it('calls refresh at interval repeatedly', () => {
|
|
const session = createMockSession({ fileTreeAutoRefreshInterval: 5 });
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(15000);
|
|
});
|
|
|
|
expect(defaultProps.refreshFileTree).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it('does not start timer when interval is 0', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(60000);
|
|
});
|
|
|
|
expect(defaultProps.refreshFileTree).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('clears timer on unmount', () => {
|
|
const session = createMockSession({ fileTreeAutoRefreshInterval: 5 });
|
|
const { unmount } = render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
unmount();
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(10000);
|
|
});
|
|
|
|
// No calls after unmount
|
|
expect(defaultProps.refreshFileTree).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('restarts timer when interval changes', () => {
|
|
const session = createMockSession({ fileTreeAutoRefreshInterval: 60 });
|
|
const { rerender } = render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(30000);
|
|
});
|
|
|
|
expect(defaultProps.refreshFileTree).not.toHaveBeenCalled();
|
|
|
|
// Change interval
|
|
const newSession = createMockSession({ fileTreeAutoRefreshInterval: 5 });
|
|
rerender(<FileExplorerPanel {...defaultProps} session={newSession} />);
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(5000);
|
|
});
|
|
|
|
expect(defaultProps.refreshFileTree).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('File Tree Rendering', () => {
|
|
it('renders folders with folder icon', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const folderIcons = screen.getAllByTestId('folder-icon');
|
|
expect(folderIcons.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('renders files with file icon', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const fileIcons = screen.getAllByTestId('file-icon');
|
|
expect(fileIcons.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('renders collapsed folders with ChevronRight', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
expect(screen.getAllByTestId('chevron-right').length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('renders expanded folders with ChevronDown', () => {
|
|
const session = createMockSession({
|
|
fileExplorerExpanded: ['src', 'src/utils']
|
|
});
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
expect(screen.getAllByTestId('chevron-down').length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('renders children when folder is expanded', () => {
|
|
const session = createMockSession({ fileExplorerExpanded: ['src'] });
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
expect(screen.getByText('index.ts')).toBeInTheDocument();
|
|
expect(screen.getByText('utils')).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not render children when folder is collapsed', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
// index.ts is inside src, which is collapsed by default
|
|
expect(screen.queryByText('index.ts')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('applies indentation to nested items', () => {
|
|
const session = createMockSession({ fileExplorerExpanded: ['src'] });
|
|
const { container } = render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
const nestedItems = container.querySelectorAll('.ml-3.border-l.pl-2');
|
|
expect(nestedItems.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('displays file name with truncate class', () => {
|
|
const { container } = render(<FileExplorerPanel {...defaultProps} />);
|
|
const truncateSpans = container.querySelectorAll('.truncate');
|
|
expect(truncateSpans.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('sets title attribute with full file name', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
expect(screen.getByTitle('src')).toBeInTheDocument();
|
|
expect(screen.getByTitle('package.json')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('File and Folder Clicks', () => {
|
|
it('calls toggleFolder when clicking a folder', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const srcFolder = screen.getByText('src');
|
|
fireEvent.click(srcFolder);
|
|
|
|
expect(defaultProps.toggleFolder).toHaveBeenCalledWith(
|
|
'src',
|
|
'session-1',
|
|
expect.any(Function)
|
|
);
|
|
});
|
|
|
|
it('sets selectedFileIndex and activeFocus when clicking a file', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const file = screen.getByText('package.json');
|
|
fireEvent.click(file);
|
|
|
|
expect(defaultProps.setSelectedFileIndex).toHaveBeenCalled();
|
|
expect(defaultProps.setActiveFocus).toHaveBeenCalledWith('right');
|
|
});
|
|
|
|
it('calls handleFileClick on double-click of file', async () => {
|
|
const session = createMockSession({ fileExplorerExpanded: ['src'] });
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
const file = screen.getByText('index.ts');
|
|
|
|
fireEvent.doubleClick(file);
|
|
|
|
expect(defaultProps.handleFileClick).toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not call handleFileClick on double-click of folder', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const folder = screen.getByText('src');
|
|
|
|
fireEvent.doubleClick(folder);
|
|
|
|
expect(defaultProps.handleFileClick).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Selected and Keyboard Selected States', () => {
|
|
it('applies selected style when file is selected in preview', () => {
|
|
const props = {
|
|
...defaultProps,
|
|
previewFile: {
|
|
name: 'package.json',
|
|
content: '{}',
|
|
path: '/Users/test/project/package.json',
|
|
},
|
|
};
|
|
const { container } = render(<FileExplorerPanel {...props} />);
|
|
const selectedItem = container.querySelector('[class*="bg-white/10"]');
|
|
expect(selectedItem).toBeInTheDocument();
|
|
});
|
|
|
|
it('applies keyboard selected style when focused', () => {
|
|
const props = {
|
|
...defaultProps,
|
|
activeFocus: 'right',
|
|
activeRightTab: 'files',
|
|
selectedFileIndex: 0,
|
|
};
|
|
const { container } = render(<FileExplorerPanel {...props} />);
|
|
const keyboardSelectedItem = container.querySelector('[data-file-index="0"]');
|
|
expect(keyboardSelectedItem).toHaveStyle({
|
|
borderLeftColor: mockTheme.colors.accent,
|
|
backgroundColor: mockTheme.colors.bgActivity
|
|
});
|
|
});
|
|
|
|
it('does not apply keyboard selected style when not focused', () => {
|
|
const props = {
|
|
...defaultProps,
|
|
activeFocus: 'main',
|
|
activeRightTab: 'files',
|
|
selectedFileIndex: 0,
|
|
};
|
|
const { container } = render(<FileExplorerPanel {...props} />);
|
|
const item = container.querySelector('[data-file-index="0"]');
|
|
// When not focused, should not have accent color (uses transparent which may not be in computed style)
|
|
expect(item).not.toHaveStyle({ borderLeftColor: mockTheme.colors.accent });
|
|
});
|
|
|
|
it('does not apply keyboard selected style when on different tab', () => {
|
|
const props = {
|
|
...defaultProps,
|
|
activeFocus: 'right',
|
|
activeRightTab: 'history',
|
|
selectedFileIndex: 0,
|
|
};
|
|
const { container } = render(<FileExplorerPanel {...props} />);
|
|
const item = container.querySelector('[data-file-index="0"]');
|
|
// When on different tab, should not have accent color
|
|
expect(item).not.toHaveStyle({ borderLeftColor: mockTheme.colors.accent });
|
|
});
|
|
});
|
|
|
|
describe('Changed Files Display', () => {
|
|
it('displays change badge for modified files', () => {
|
|
const session = createMockSession({
|
|
changedFiles: [{ path: '/Users/test/project/package.json', type: 'modified' }],
|
|
});
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
expect(screen.getByText('modified')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays change badge for added files', () => {
|
|
const session = createMockSession({
|
|
changedFiles: [{ path: '/Users/test/project/package.json', type: 'added' }],
|
|
});
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
expect(screen.getByText('added')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays change badge for deleted files', () => {
|
|
const session = createMockSession({
|
|
changedFiles: [{ path: '/Users/test/project/package.json', type: 'deleted' }],
|
|
});
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
expect(screen.getByText('deleted')).toBeInTheDocument();
|
|
});
|
|
|
|
it('applies success color to added badge', () => {
|
|
const session = createMockSession({
|
|
changedFiles: [{ path: '/Users/test/project/package.json', type: 'added' }],
|
|
});
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
const badge = screen.getByText('added');
|
|
expect(badge).toHaveStyle({ color: mockTheme.colors.success });
|
|
});
|
|
|
|
it('applies warning color to modified badge', () => {
|
|
const session = createMockSession({
|
|
changedFiles: [{ path: '/Users/test/project/package.json', type: 'modified' }],
|
|
});
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
const badge = screen.getByText('modified');
|
|
expect(badge).toHaveStyle({ color: mockTheme.colors.warning });
|
|
});
|
|
|
|
it('applies error color to deleted badge', () => {
|
|
const session = createMockSession({
|
|
changedFiles: [{ path: '/Users/test/project/package.json', type: 'deleted' }],
|
|
});
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
const badge = screen.getByText('deleted');
|
|
expect(badge).toHaveStyle({ color: mockTheme.colors.error });
|
|
});
|
|
|
|
it('applies bold font to changed file names', () => {
|
|
const session = createMockSession({
|
|
changedFiles: [{ path: '/Users/test/project/package.json', type: 'modified' }],
|
|
});
|
|
const { container } = render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
const boldItems = container.querySelectorAll('.font-medium');
|
|
expect(boldItems.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('applies textMain color to changed file names', () => {
|
|
const session = createMockSession({
|
|
changedFiles: [{ path: '/Users/test/project/package.json', type: 'modified' }],
|
|
});
|
|
const { container } = render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
// Find item with package.json
|
|
const fileItem = Array.from(container.querySelectorAll('[data-file-index]'))
|
|
.find(el => el.textContent?.includes('package.json'));
|
|
expect(fileItem).toHaveStyle({ color: mockTheme.colors.textMain });
|
|
});
|
|
});
|
|
|
|
describe('Error State', () => {
|
|
it('displays error message when fileTreeError is set', () => {
|
|
const session = createMockSession({
|
|
fileTreeError: 'Directory not found'
|
|
});
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
expect(screen.getByText('Directory not found')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows Select New Directory button on error', () => {
|
|
const session = createMockSession({
|
|
fileTreeError: 'Permission denied'
|
|
});
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
expect(screen.getByText('Select New Directory')).toBeInTheDocument();
|
|
});
|
|
|
|
it('calls updateSessionWorkingDirectory when Select New Directory is clicked', () => {
|
|
const session = createMockSession({
|
|
fileTreeError: 'Permission denied'
|
|
});
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
const button = screen.getByText('Select New Directory');
|
|
fireEvent.click(button);
|
|
|
|
expect(defaultProps.updateSessionWorkingDirectory).toHaveBeenCalledWith(
|
|
'session-1',
|
|
expect.any(Function)
|
|
);
|
|
});
|
|
|
|
it('applies error color to error message', () => {
|
|
const session = createMockSession({
|
|
fileTreeError: 'Error message'
|
|
});
|
|
const { container } = render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
const errorDiv = container.querySelector('[class*="text-center"]');
|
|
expect(errorDiv).toHaveStyle({ color: mockTheme.colors.error });
|
|
});
|
|
|
|
it('does not show file tree when error is present', () => {
|
|
const session = createMockSession({
|
|
fileTreeError: 'Error message'
|
|
});
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
expect(screen.queryByText('src')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('package.json')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Empty State', () => {
|
|
it('shows loading message when fileTree is empty and no error', () => {
|
|
const session = createMockSession({ fileTree: [] });
|
|
render(
|
|
<FileExplorerPanel
|
|
{...defaultProps}
|
|
session={session}
|
|
filteredFileTree={[]}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText('Loading files...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows loading message when fileTree is null', () => {
|
|
const session = createMockSession({ fileTree: undefined as any });
|
|
render(
|
|
<FileExplorerPanel
|
|
{...defaultProps}
|
|
session={session}
|
|
filteredFileTree={undefined as any}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText('Loading files...')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Portal Overlay Position', () => {
|
|
it('calculates overlay position from button rect', async () => {
|
|
// Mock getBoundingClientRect
|
|
const mockRect = {
|
|
top: 100,
|
|
left: 200,
|
|
bottom: 130,
|
|
right: 230,
|
|
width: 30,
|
|
height: 30,
|
|
x: 200,
|
|
y: 100,
|
|
toJSON: () => {},
|
|
};
|
|
|
|
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(mockRect);
|
|
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
act(() => {
|
|
vi.advanceTimersByTime(400);
|
|
});
|
|
|
|
const overlay = screen.getByText('Auto-refresh').closest('.fixed');
|
|
expect(overlay).toHaveStyle({
|
|
top: '134px', // bottom + 4
|
|
left: '230px', // right
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Theme Styling', () => {
|
|
it('applies bgSidebar to overlay background', async () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
act(() => {
|
|
vi.advanceTimersByTime(400);
|
|
});
|
|
|
|
const overlay = screen.getByText('Auto-refresh').closest('.fixed');
|
|
expect(overlay).toHaveStyle({ backgroundColor: mockTheme.colors.bgSidebar });
|
|
});
|
|
|
|
it('applies border color to overlay', async () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
act(() => {
|
|
vi.advanceTimersByTime(400);
|
|
});
|
|
|
|
const overlay = screen.getByText('Auto-refresh').closest('.fixed');
|
|
expect(overlay).toHaveStyle({ borderColor: mockTheme.colors.border });
|
|
});
|
|
|
|
it('applies bgActivity to overlay header', async () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
act(() => {
|
|
vi.advanceTimersByTime(400);
|
|
});
|
|
|
|
const header = screen.getByText('Auto-refresh');
|
|
expect(header).toHaveStyle({ backgroundColor: mockTheme.colors.bgActivity });
|
|
});
|
|
|
|
it('applies textDim to unchanged file names', () => {
|
|
const { container } = render(<FileExplorerPanel {...defaultProps} />);
|
|
|
|
// First item should be src (unchanged folder)
|
|
const item = container.querySelector('[data-file-index="0"]');
|
|
expect(item).toHaveStyle({ color: mockTheme.colors.textDim });
|
|
});
|
|
});
|
|
|
|
describe('Nested File Tree', () => {
|
|
it('renders deeply nested structure when all expanded', () => {
|
|
const session = createMockSession({
|
|
fileExplorerExpanded: ['src', 'src/utils']
|
|
});
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
expect(screen.getByText('src')).toBeInTheDocument();
|
|
expect(screen.getByText('index.ts')).toBeInTheDocument();
|
|
expect(screen.getByText('utils')).toBeInTheDocument();
|
|
expect(screen.getByText('helpers.ts')).toBeInTheDocument();
|
|
});
|
|
|
|
it('tracks global index correctly through nested items', () => {
|
|
const session = createMockSession({
|
|
fileExplorerExpanded: ['src', 'src/utils']
|
|
});
|
|
const { container } = render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
// Check indices are sequential
|
|
const items = container.querySelectorAll('[data-file-index]');
|
|
const indices = Array.from(items).map(el => parseInt(el.getAttribute('data-file-index')!, 10));
|
|
|
|
// Should be sequential: 0, 1, 2, 3, 4...
|
|
for (let i = 0; i < indices.length; i++) {
|
|
expect(indices[i]).toBe(i);
|
|
}
|
|
});
|
|
|
|
it('correctly builds full paths for nested items', () => {
|
|
const session = createMockSession({
|
|
fileExplorerExpanded: ['src', 'src/utils']
|
|
});
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
// Double-click helpers.ts to verify path building
|
|
const helpersFile = screen.getByText('helpers.ts');
|
|
fireEvent.doubleClick(helpersFile);
|
|
|
|
expect(defaultProps.handleFileClick).toHaveBeenCalledWith(
|
|
expect.objectContaining({ name: 'helpers.ts' }),
|
|
'src/utils/helpers.ts',
|
|
expect.any(Object)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Folder Toggle Path Building', () => {
|
|
it('builds correct path for root-level folders', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
const srcFolder = screen.getByText('src');
|
|
fireEvent.click(srcFolder);
|
|
|
|
expect(defaultProps.toggleFolder).toHaveBeenCalledWith(
|
|
'src',
|
|
'session-1',
|
|
expect.any(Function)
|
|
);
|
|
});
|
|
|
|
it('builds correct path for nested folders', () => {
|
|
const session = createMockSession({ fileExplorerExpanded: ['src'] });
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
const utilsFolder = screen.getByText('utils');
|
|
fireEvent.click(utilsFolder);
|
|
|
|
expect(defaultProps.toggleFolder).toHaveBeenCalledWith(
|
|
'src/utils',
|
|
'session-1',
|
|
expect.any(Function)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('handles undefined fileExplorerExpanded', () => {
|
|
const session = createMockSession({ fileExplorerExpanded: undefined as any });
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
// Should render without crashing
|
|
expect(screen.getByText('src')).toBeInTheDocument();
|
|
});
|
|
|
|
// TODO: PENDING - NEEDS FIX - Source code at line 201 doesn't handle undefined changedFiles
|
|
// The code does: session.changedFiles.find(...) without null check
|
|
it.skip('handles undefined changedFiles', () => {
|
|
const session = createMockSession({ changedFiles: undefined as any });
|
|
const { container } = render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
// Should render without crashing
|
|
expect(container).toBeTruthy();
|
|
});
|
|
|
|
it('handles empty filteredFileTree', () => {
|
|
render(<FileExplorerPanel {...defaultProps} filteredFileTree={[]} />);
|
|
expect(screen.getByText('Loading files...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('handles null previewFile', () => {
|
|
render(<FileExplorerPanel {...defaultProps} previewFile={null} />);
|
|
expect(screen.getByText('src')).toBeInTheDocument();
|
|
});
|
|
|
|
it('handles very long cwd path', () => {
|
|
const longPath = '/Users/test/very/long/path/to/project/that/is/really/deep';
|
|
const session = createMockSession({ cwd: longPath });
|
|
render(<FileExplorerPanel {...defaultProps} session={session} />);
|
|
|
|
expect(screen.getByTitle(longPath)).toBeInTheDocument();
|
|
});
|
|
|
|
it('handles special characters in file names', () => {
|
|
const specialFileTree = [
|
|
{ name: 'file with spaces.ts', type: 'file' as const },
|
|
{ name: 'file-with-dashes.ts', type: 'file' as const },
|
|
{ name: 'file_with_underscores.ts', type: 'file' as const },
|
|
];
|
|
render(<FileExplorerPanel {...defaultProps} filteredFileTree={specialFileTree} />);
|
|
|
|
expect(screen.getByText('file with spaces.ts')).toBeInTheDocument();
|
|
expect(screen.getByText('file-with-dashes.ts')).toBeInTheDocument();
|
|
expect(screen.getByText('file_with_underscores.ts')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Optional Props', () => {
|
|
it('works without onAutoRefreshChange', () => {
|
|
const props = { ...defaultProps };
|
|
delete props.onAutoRefreshChange;
|
|
|
|
render(<FileExplorerPanel {...props} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
fireEvent.mouseEnter(refreshButton);
|
|
act(() => {
|
|
vi.advanceTimersByTime(400);
|
|
});
|
|
|
|
const option = screen.getByText('Every 5 seconds');
|
|
// Should not throw
|
|
fireEvent.click(option);
|
|
});
|
|
|
|
it('works without onShowFlash', async () => {
|
|
const props = { ...defaultProps };
|
|
delete props.onShowFlash;
|
|
|
|
render(<FileExplorerPanel {...props} />);
|
|
const refreshButton = screen.getByTitle('Refresh file tree');
|
|
|
|
// Should not throw
|
|
await act(async () => {
|
|
fireEvent.click(refreshButton);
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
});
|
|
});
|
|
|
|
it('works without refs', () => {
|
|
const props = {
|
|
...defaultProps,
|
|
fileTreeContainerRef: undefined,
|
|
fileTreeFilterInputRef: undefined,
|
|
};
|
|
|
|
render(<FileExplorerPanel {...props} />);
|
|
expect(screen.getByText('src')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Accessibility', () => {
|
|
it('uses button elements for interactive controls', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
|
|
const buttons = screen.getAllByRole('button');
|
|
expect(buttons.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('has title attributes for screen readers', () => {
|
|
render(<FileExplorerPanel {...defaultProps} />);
|
|
|
|
expect(screen.getByTitle('Refresh file tree')).toBeInTheDocument();
|
|
expect(screen.getByTitle('Expand all folders')).toBeInTheDocument();
|
|
expect(screen.getByTitle('Collapse all folders')).toBeInTheDocument();
|
|
});
|
|
|
|
it('uses input type text for filter', () => {
|
|
render(<FileExplorerPanel {...defaultProps} fileTreeFilterOpen={true} />);
|
|
|
|
const input = screen.getByPlaceholderText('Filter files...');
|
|
expect(input).toHaveAttribute('type', 'text');
|
|
});
|
|
|
|
it('has ariaLabel on layer registration', () => {
|
|
render(<FileExplorerPanel {...defaultProps} fileTreeFilterOpen={true} />);
|
|
|
|
expect(mockRegisterLayer).toHaveBeenCalledWith(
|
|
expect.objectContaining({ ariaLabel: 'File Tree Filter' })
|
|
);
|
|
});
|
|
});
|
|
});
|