/** * Tests for the ProjectDetailPage component. * Covers: isSlicedFilename conditional print-button logic, linked folder file rendering, * and the PrintModal open trigger with projectId. */ /// import { describe, it, expect, beforeEach, vi } from 'vitest'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { render } from '../utils'; import { ProjectDetailPage } from '../../pages/ProjectDetailPage'; import { http, HttpResponse } from 'msw'; import { server } from '../mocks/server'; // Mock useParams so the component receives a fixed project id without a nested Router vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); return { ...actual, useParams: () => ({ id: '1' }), useNavigate: () => vi.fn(), }; }); const mockProject = { id: 1, name: 'Test Project', description: 'A test project', color: '#00ae42', status: 'active', priority: 'normal', due_date: null, notes: null, parent_id: null, archive_count: 0, total_print_time_seconds: 0, total_filament_grams: 0, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }; const mockFolder = { id: 10, name: 'Sliced Files', project_id: 1, archive_id: null, parent_id: null, file_count: 3, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }; function makeFile(overrides: { id: number; filename: string; file_type?: string }) { return { id: overrides.id, filename: overrides.filename, print_name: null, file_type: overrides.file_type ?? '3mf', folder_id: 10, project_id: 1, file_hash: null, file_size_bytes: 1024, thumbnail_path: null, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', duplicate_count: 0, }; } describe('ProjectDetailPage', () => { beforeEach(() => { server.use( http.get('/api/v1/projects/:id', () => { return HttpResponse.json(mockProject); }), http.get('/api/v1/projects/:id/archives', () => { return HttpResponse.json([]); }), http.get('/api/v1/projects/:id/bom', () => { return HttpResponse.json([]); }), http.get('/api/v1/projects/:id/timeline', () => { return HttpResponse.json([]); }), http.get('/api/v1/library/folders/by-project/:id', () => { return HttpResponse.json([mockFolder]); }), ); }); describe('isSlicedFilename — conditional print button', () => { it('shows print button for .gcode files', async () => { server.use( http.get('/api/v1/library/files', () => { return HttpResponse.json([makeFile({ id: 1, filename: 'benchy.gcode', file_type: 'gcode' })]); }) ); render(); await waitFor(() => { expect(screen.getByTitle('Print Now')).toBeInTheDocument(); }); }); it('shows print button for .gcode.3mf files', async () => { server.use( http.get('/api/v1/library/files', () => { return HttpResponse.json([makeFile({ id: 2, filename: 'benchy.gcode.3mf', file_type: '3mf' })]); }) ); render(); await waitFor(() => { expect(screen.getByTitle('Print Now')).toBeInTheDocument(); }); }); it('does NOT show print button for .gcode.bak files (regression for includes bug)', async () => { server.use( http.get('/api/v1/library/files', () => { return HttpResponse.json([makeFile({ id: 3, filename: 'benchy.gcode.bak', file_type: '3mf' })]); }) ); render(); await waitFor(() => { expect(screen.getByText('benchy.gcode.bak')).toBeInTheDocument(); }); expect(screen.queryByTitle('Print Now')).not.toBeInTheDocument(); }); it('does NOT show print button for .stl files', async () => { server.use( http.get('/api/v1/library/files', () => { return HttpResponse.json([makeFile({ id: 4, filename: 'model.stl', file_type: 'stl' })]); }) ); render(); await waitFor(() => { expect(screen.getByText('model.stl')).toBeInTheDocument(); }); expect(screen.queryByTitle('Print Now')).not.toBeInTheDocument(); }); }); describe('linked folder file rendering', () => { it('renders filenames from linked folder', async () => { server.use( http.get('/api/v1/library/files', () => { return HttpResponse.json([ makeFile({ id: 5, filename: 'part_a.gcode.3mf', file_type: '3mf' }), makeFile({ id: 6, filename: 'design.stl', file_type: 'stl' }), ]); }) ); render(); await waitFor(() => { expect(screen.getByText('part_a.gcode.3mf')).toBeInTheDocument(); expect(screen.getByText('design.stl')).toBeInTheDocument(); }); }); it('renders the linked folder name', async () => { server.use( http.get('/api/v1/library/files', () => { return HttpResponse.json([]); }) ); render(); await waitFor(() => { expect(screen.getByText('Sliced Files')).toBeInTheDocument(); }); }); }); describe('print modal trigger', () => { it('opens PrintModal when print button is clicked on a sliced file', async () => { const user = userEvent.setup(); server.use( http.get('/api/v1/library/files', () => { return HttpResponse.json([makeFile({ id: 7, filename: 'cube.gcode.3mf', file_type: '3mf' })]); }), http.get('/api/v1/printers/', () => { return HttpResponse.json([]); }), http.get('/api/v1/library/files/:id', () => { return HttpResponse.json(makeFile({ id: 7, filename: 'cube.gcode.3mf', file_type: '3mf' })); }), http.get('/api/v1/library/files/:id/plates', () => { return HttpResponse.json({ is_multi_plate: false, plates: [] }); }), http.get('/api/v1/library/files/:id/filament-requirements', () => { return HttpResponse.json({ file_id: 7, filename: 'cube.gcode.3mf', filaments: [] }); }), ); render(); await waitFor(() => { expect(screen.getByTitle('Print Now')).toBeInTheDocument(); }); await user.click(screen.getByTitle('Print Now')); // PrintModal should open — look for the modal heading "Print" await waitFor(() => { expect(screen.getByRole('heading', { name: 'Print' })).toBeInTheDocument(); }); }); }); });