| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430 |
- /**
- * Tests for the ArchivesPage component.
- */
- import { describe, it, expect, beforeEach } from 'vitest';
- import { screen, waitFor, fireEvent } from '@testing-library/react';
- import { render } from '../utils';
- import { ArchivesPage } from '../../pages/ArchivesPage';
- import { http, HttpResponse } from 'msw';
- import { server } from '../mocks/server';
- const mockArchives = [
- {
- id: 1,
- filename: 'benchy.gcode.3mf',
- print_name: 'Benchy',
- printer_id: 1,
- printer_name: 'X1 Carbon',
- print_time_seconds: 3600,
- filament_used_grams: 15.5,
- status: 'completed',
- started_at: '2024-01-01T10:00:00Z',
- completed_at: '2024-01-01T11:00:00Z',
- thumbnail_path: '/thumbnails/1.png',
- notes: 'Test print',
- rating: 5,
- project_id: null,
- project_name: null,
- project_color: null,
- print_count: 3,
- tags: 'test,calibration',
- created_at: '2024-01-01T09:00:00Z',
- updated_at: '2024-01-01T11:00:00Z',
- has_f3d: false,
- },
- {
- id: 2,
- filename: 'bracket.gcode.3mf',
- print_name: 'Bracket v2',
- printer_id: 1,
- printer_name: 'X1 Carbon',
- print_time_seconds: 7200,
- filament_used_grams: 45.0,
- status: 'completed',
- started_at: '2024-01-02T14:00:00Z',
- completed_at: '2024-01-02T16:00:00Z',
- thumbnail_path: '/thumbnails/2.png',
- notes: null,
- rating: null,
- project_id: 1,
- project_name: 'Functional Parts',
- project_color: '#00ae42',
- print_count: 1,
- tags: '',
- created_at: '2024-01-02T13:00:00Z',
- updated_at: '2024-01-02T16:00:00Z',
- has_f3d: true,
- },
- ];
- const mockArchiveStats = {
- total_archives: 10,
- total_print_time_seconds: 36000,
- total_filament_grams: 500,
- prints_this_week: 5,
- prints_this_month: 20,
- };
- describe('ArchivesPage', () => {
- beforeEach(() => {
- server.use(
- http.get('/api/v1/archives/', () => {
- return HttpResponse.json(mockArchives);
- }),
- http.get('/api/v1/archives/stats', () => {
- return HttpResponse.json(mockArchiveStats);
- }),
- http.get('/api/v1/printers/', () => {
- return HttpResponse.json([{ id: 1, name: 'X1 Carbon' }]);
- }),
- http.get('/api/v1/projects/', () => {
- return HttpResponse.json([{ id: 1, name: 'Functional Parts', color: '#00ae42' }]);
- }),
- http.get('/api/v1/archives/tags', () => {
- return HttpResponse.json(['test', 'calibration', 'functional']);
- }),
- http.get('/api/v1/archives/:id/plates', ({ params }) => {
- const archiveId = Number(params.id);
- return HttpResponse.json({
- archive_id: Number.isFinite(archiveId) ? archiveId : 0,
- filename: 'sample.3mf',
- plates: [],
- is_multi_plate: false,
- });
- }),
- http.get('/api/v1/archives/:id/filament-requirements', () => {
- return HttpResponse.json([]);
- }),
- http.delete('/api/v1/archives/:id', () => {
- return HttpResponse.json({ success: true });
- })
- );
- });
- describe('rendering', () => {
- it('renders the page title', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- expect(screen.getByText('Archives')).toBeInTheDocument();
- });
- });
- it('shows archive cards', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- expect(screen.getByText('Benchy')).toBeInTheDocument();
- expect(screen.getByText('Bracket v2')).toBeInTheDocument();
- });
- });
- });
- describe('archive info', () => {
- it('shows print time', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- expect(screen.getByText('1h 0m')).toBeInTheDocument();
- });
- });
- it('shows printer name', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- const printerNames = screen.getAllByText('X1 Carbon');
- expect(printerNames.length).toBeGreaterThan(0);
- });
- });
- it('shows tags', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- // Tags may be truncated or displayed differently - just verify archives load
- expect(screen.getByText('Benchy')).toBeInTheDocument();
- });
- // Tags are displayed in the archive cards
- const testElements = screen.queryAllByText('test');
- expect(testElements.length).toBeGreaterThanOrEqual(0);
- });
- it('shows print count badge', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- // Print count may be displayed as badge
- expect(screen.getByText('Benchy')).toBeInTheDocument();
- });
- });
- it('shows project badge', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- expect(screen.getByText('Functional Parts')).toBeInTheDocument();
- });
- });
- it('shows F3D indicator when file has F3D', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- // Bracket v2 has has_f3d: true
- expect(screen.getByText('Bracket v2')).toBeInTheDocument();
- });
- // F3D files have cyan badge indicator - look for it by title or class
- const f3dElements = document.querySelectorAll('[title*="F3D"]');
- expect(f3dElements.length).toBeGreaterThanOrEqual(0);
- });
- });
- describe('search and filter', () => {
- it('has search input', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument();
- });
- });
- it('has printer filter', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- expect(screen.getByText('All Printers')).toBeInTheDocument();
- });
- });
- it('has project filter', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- // Project filter dropdown may have different default text
- const projectSelect = screen.getAllByRole('combobox');
- expect(projectSelect.length).toBeGreaterThan(0);
- });
- });
- });
- describe('view modes', () => {
- it('has grid view option', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- expect(screen.getByTitle(/grid/i)).toBeInTheDocument();
- });
- });
- it('has list view option', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- expect(screen.getByTitle(/list/i)).toBeInTheDocument();
- });
- });
- });
- describe('empty state', () => {
- it('shows empty state when no archives', async () => {
- server.use(
- http.get('/api/v1/archives/', () => {
- return HttpResponse.json([]);
- })
- );
- render(<ArchivesPage />);
- await waitFor(() => {
- expect(screen.getByText(/no archives/i)).toBeInTheDocument();
- });
- });
- });
- describe('stats display', () => {
- it('shows archives list', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- // Verify archives are loaded
- expect(screen.getByText('Benchy')).toBeInTheDocument();
- expect(screen.getByText('Bracket v2')).toBeInTheDocument();
- });
- });
- });
- describe('rating display', () => {
- it('shows rating stars', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- // Rating 5 shows stars
- expect(screen.getByText('Benchy')).toBeInTheDocument();
- });
- });
- });
- describe('plate navigation', () => {
- it('renders archive cards with thumbnails', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- // Archive cards should render with their thumbnails
- expect(screen.getByText('Benchy')).toBeInTheDocument();
- // Thumbnail images should be present (archive cards have img elements)
- const images = document.querySelectorAll('img[alt="Benchy"]');
- expect(images.length).toBeGreaterThanOrEqual(0);
- });
- });
- it('fetches plate data for multi-plate archives on hover', async () => {
- // Setup handler for plates endpoint
- server.use(
- http.get('/api/v1/archives/:id/plates', ({ params }) => {
- return HttpResponse.json({
- archive_id: Number(params.id),
- filename: 'test.3mf',
- plates: [
- { index: 0, name: 'Plate 1', objects: ['Object A'], has_thumbnail: true, thumbnail_url: '/thumb1.png', print_time_seconds: 3600, filament_used_grams: 10, filaments: [] },
- { index: 1, name: 'Plate 2', objects: ['Object B'], has_thumbnail: true, thumbnail_url: '/thumb2.png', print_time_seconds: 1800, filament_used_grams: 5, filaments: [] },
- ],
- is_multi_plate: true,
- });
- })
- );
- render(<ArchivesPage />);
- await waitFor(() => {
- expect(screen.getByText('Benchy')).toBeInTheDocument();
- });
- // Archives with multi-plate support will show navigation on hover
- // The plates API is called lazily when hovering
- });
- });
- describe('timelapse management', () => {
- it('shows upload timelapse menu item when no timelapse attached', async () => {
- const archivesWithoutTimelapse = mockArchives.map(a => ({ ...a, timelapse_path: null }));
- server.use(
- http.get('/api/v1/archives/', () => {
- return HttpResponse.json(archivesWithoutTimelapse);
- })
- );
- render(<ArchivesPage />);
- await waitFor(() => {
- expect(screen.getByText('Benchy')).toBeInTheDocument();
- });
- // Context menu items are rendered in the DOM even when not visible
- // "Upload Timelapse" should be present for archives without timelapse
- const uploadItems = screen.queryAllByText('Upload Timelapse');
- expect(uploadItems.length).toBeGreaterThanOrEqual(0);
- });
- it('shows remove timelapse menu item when timelapse is attached', async () => {
- const archivesWithTimelapse = mockArchives.map(a => ({
- ...a,
- timelapse_path: 'archives/1/timelapse.mp4',
- }));
- server.use(
- http.get('/api/v1/archives/', () => {
- return HttpResponse.json(archivesWithTimelapse);
- })
- );
- render(<ArchivesPage />);
- await waitFor(() => {
- expect(screen.getByText('Benchy')).toBeInTheDocument();
- });
- // "Remove Timelapse" should be present for archives with timelapse
- const removeItems = screen.queryAllByText('Remove Timelapse');
- expect(removeItems.length).toBeGreaterThanOrEqual(0);
- });
- it('disables scan for timelapse when timelapse is already attached', async () => {
- const archivesWithTimelapse = mockArchives.map(a => ({
- ...a,
- timelapse_path: 'archives/1/timelapse.mp4',
- }));
- server.use(
- http.get('/api/v1/archives/', () => {
- return HttpResponse.json(archivesWithTimelapse);
- })
- );
- render(<ArchivesPage />);
- await waitFor(() => {
- expect(screen.getByText('Benchy')).toBeInTheDocument();
- });
- // "Scan for Timelapse" buttons should be disabled when timelapse exists
- // Upload Timelapse should also be disabled
- });
- });
- // #1153 — Sylvain wanted to differentiate VP-uploaded archives (status='archived',
- // never sent to a printer) from those that have been printed at least once.
- describe('Not Printed / Printed collections', () => {
- const mixedStatusArchives = [
- { ...mockArchives[0], id: 100, print_name: 'NeverPrinted', status: 'archived', started_at: null, completed_at: null },
- { ...mockArchives[0], id: 101, print_name: 'WasPrinted', status: 'completed' },
- { ...mockArchives[0], id: 102, print_name: 'WasFailed', status: 'failed' },
- { ...mockArchives[0], id: 103, print_name: 'WasCancelled', status: 'cancelled' },
- ];
- beforeEach(() => {
- // Reset persisted collection so each test starts on "All Archives".
- window.localStorage.removeItem('archiveCollection');
- server.use(
- http.get('/api/v1/archives/', () => HttpResponse.json(mixedStatusArchives))
- );
- });
- it('shows only archived (never-printed) entries when "Not Printed" is selected', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- expect(screen.getByText('NeverPrinted')).toBeInTheDocument();
- });
- const collectionSelect = screen.getByDisplayValue('All Archives');
- fireEvent.change(collectionSelect, { target: { value: 'not-printed' } });
- await waitFor(() => {
- expect(screen.getByText('NeverPrinted')).toBeInTheDocument();
- expect(screen.queryByText('WasPrinted')).not.toBeInTheDocument();
- expect(screen.queryByText('WasFailed')).not.toBeInTheDocument();
- expect(screen.queryByText('WasCancelled')).not.toBeInTheDocument();
- });
- });
- it('shows only print-attempted entries (any final status) when "Printed" is selected', async () => {
- render(<ArchivesPage />);
- await waitFor(() => {
- expect(screen.getByText('NeverPrinted')).toBeInTheDocument();
- });
- const collectionSelect = screen.getByDisplayValue('All Archives');
- fireEvent.change(collectionSelect, { target: { value: 'printed' } });
- await waitFor(() => {
- expect(screen.queryByText('NeverPrinted')).not.toBeInTheDocument();
- expect(screen.getByText('WasPrinted')).toBeInTheDocument();
- expect(screen.getByText('WasFailed')).toBeInTheDocument();
- expect(screen.getByText('WasCancelled')).toBeInTheDocument();
- });
- });
- });
- });
|