/** * Tests for the FileManagerModal component. * Tests file browsing, selection, navigation, and file operations. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { screen, fireEvent, waitFor } from '@testing-library/react'; import { render } from '../utils'; import { FileManagerModal } from '../../components/FileManagerModal'; import { http, HttpResponse } from 'msw'; import { server } from '../mocks/server'; const mockFiles = [ { name: 'cache', path: '/cache', size: 0, is_directory: true, mtime: '2024-01-15T10:00:00Z', }, { name: 'model', path: '/model', size: 0, is_directory: true, mtime: '2024-01-15T10:00:00Z', }, { name: 'benchy.3mf', path: '/benchy.3mf', size: 1048575, is_directory: false, mtime: '2024-01-15T10:00:00Z', }, { name: 'print_job.gcode', path: '/print_job.gcode', size: 2048000, is_directory: false, mtime: '2024-01-14T10:00:00Z', }, ]; const mockStorage = { used_bytes: 1073741824, // 1 GB free_bytes: 3221225472, // 3 GB }; describe('FileManagerModal', () => { const mockOnClose = vi.fn(); beforeEach(() => { vi.clearAllMocks(); server.use( http.get('/api/v1/printers/:id/files', () => { return HttpResponse.json({ files: mockFiles }); }), http.get('/api/v1/printers/:id/storage', () => { return HttpResponse.json(mockStorage); }), http.delete('/api/v1/printers/:id/files', () => { return HttpResponse.json({ success: true }); }) ); }); describe('rendering', () => { it('renders the modal with header', async () => { render( ); expect(screen.getByText('File Manager')).toBeInTheDocument(); expect(screen.getByText('X1 Carbon')).toBeInTheDocument(); }); it('renders storage info', async () => { render( ); await waitFor(() => { expect(screen.getByText(/Used:/)).toBeInTheDocument(); expect(screen.getByText(/Free:/)).toBeInTheDocument(); }); }); it('renders quick navigation buttons', () => { render( ); expect(screen.getByText('Root')).toBeInTheDocument(); expect(screen.getByText('Cache')).toBeInTheDocument(); expect(screen.getByText('Models')).toBeInTheDocument(); expect(screen.getByText('Timelapse')).toBeInTheDocument(); }); it('renders file list', async () => { render( ); await waitFor(() => { expect(screen.getByText('cache')).toBeInTheDocument(); expect(screen.getByText('model')).toBeInTheDocument(); expect(screen.getByText('benchy.3mf')).toBeInTheDocument(); expect(screen.getByText('print_job.gcode')).toBeInTheDocument(); }); }); it('shows file sizes for files', async () => { render( ); await waitFor(() => { // 1024000 bytes = 1024.0 KB expect(screen.getByText('1024.0 KB')).toBeInTheDocument(); }); }); }); describe('navigation', () => { it('navigates into a folder when clicked', async () => { server.use( http.get('/api/v1/printers/:id/files', ({ request }) => { const url = new URL(request.url); const path = url.searchParams.get('path'); if (path === '/cache') { return HttpResponse.json({ files: [ { name: 'temp.dat', path: '/cache/temp.dat', size: 512, is_directory: false }, ], }); } return HttpResponse.json({ files: mockFiles }); }) ); render( ); await waitFor(() => { expect(screen.getByText('cache')).toBeInTheDocument(); }); // Click on cache folder fireEvent.click(screen.getByText('cache')); await waitFor(() => { expect(screen.getByText('temp.dat')).toBeInTheDocument(); }); }); it('shows current path', async () => { render( ); expect(screen.getByText('/')).toBeInTheDocument(); }); }); describe('file selection', () => { it('selects a file when checkbox is clicked', async () => { render( ); await waitFor(() => { expect(screen.getByText('benchy.3mf')).toBeInTheDocument(); }); // Find and click a checkbox (files have checkboxes, directories don't) const checkboxes = screen.getAllByRole('button').filter(btn => btn.querySelector('svg')?.classList.contains('lucide-square') ); if (checkboxes.length > 0) { fireEvent.click(checkboxes[0]); await waitFor(() => { expect(screen.getByText('1 selected')).toBeInTheDocument(); }); } }); it('enables download button when files are selected', async () => { render( ); await waitFor(() => { expect(screen.getByText('benchy.3mf')).toBeInTheDocument(); }); // Download button should be disabled initially const downloadButton = screen.getByRole('button', { name: /Download/i }); expect(downloadButton).toBeDisabled(); }); it('shows Select All button when files exist', async () => { render( ); await waitFor(() => { expect(screen.getByText('Select All')).toBeInTheDocument(); }); }); }); describe('search and filter', () => { it('renders search input', () => { render( ); expect(screen.getByPlaceholderText('Filter files...')).toBeInTheDocument(); }); it('filters files based on search query', async () => { render( ); await waitFor(() => { expect(screen.getByText('benchy.3mf')).toBeInTheDocument(); }); const searchInput = screen.getByPlaceholderText('Filter files...'); fireEvent.change(searchInput, { target: { value: 'benchy' } }); await waitFor(() => { expect(screen.getByText('benchy.3mf')).toBeInTheDocument(); expect(screen.queryByText('print_job.gcode')).not.toBeInTheDocument(); }); }); }); describe('sorting', () => { it('renders sort dropdown', () => { render( ); expect(screen.getByRole('combobox')).toBeInTheDocument(); }); it('has sort options available', () => { render( ); const sortSelect = screen.getByRole('combobox'); expect(sortSelect).toBeInTheDocument(); // Check that options exist expect(screen.getByText('Name (A-Z)')).toBeInTheDocument(); }); }); describe('close behavior', () => { it('calls onClose when X button is clicked', async () => { render( ); const closeButton = screen.getAllByRole('button').find(btn => btn.querySelector('.lucide-x') ); if (closeButton) { fireEvent.click(closeButton); expect(mockOnClose).toHaveBeenCalled(); } }); it('calls onClose when clicking outside the modal', () => { render( ); // Click on the backdrop const backdrop = document.querySelector('.fixed.inset-0'); if (backdrop) { fireEvent.click(backdrop); expect(mockOnClose).toHaveBeenCalled(); } }); it('calls onClose when Escape key is pressed', () => { render( ); fireEvent.keyDown(window, { key: 'Escape' }); expect(mockOnClose).toHaveBeenCalled(); }); }); describe('empty state', () => { it('shows empty message when directory has no files', async () => { server.use( http.get('/api/v1/printers/:id/files', () => { return HttpResponse.json({ files: [] }); }) ); render( ); await waitFor(() => { expect(screen.getByText('No files in this directory')).toBeInTheDocument(); }); }); }); describe('loading state', () => { it('shows loading spinner while fetching files', () => { // Delay the response to see loading state server.use( http.get('/api/v1/printers/:id/files', async () => { await new Promise((r) => setTimeout(r, 100)); return HttpResponse.json({ files: mockFiles }); }) ); render( ); // The loader should be present initially const loader = document.querySelector('.animate-spin'); expect(loader).toBeInTheDocument(); }); }); });