import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ArchiveMediaDownloadModal } from '../../components/ArchiveMediaDownloadModal'; import { api } from '../../api/client'; const showToast = vi.fn(); vi.mock('../../contexts/ToastContext', () => ({ useToast: () => ({ showToast }), })); vi.mock('../../api/client', () => ({ api: { getArchivePrinterMedia: vi.fn(), downloadArchiveTimelapse: vi.fn(), downloadPrinterFilesAsZip: vi.fn(), }, })); const media = { archive_id: 1, printer_id: 1, local_timelapse: null, remote_files: [{ name: 'video.mp4', path: '/timelapse/video.mp4', size: 1024, mtime: '2026-08-18T10:00:00Z', kind: 'timelapse' as const, }], warnings: [], }; describe('ArchiveMediaDownloadModal', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(api.getArchivePrinterMedia).mockResolvedValue(media); }); it('does not reselect a manually deselected single file when query data refreshes', async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render( , ); const filename = await screen.findByText('video.mp4'); const downloadButton = screen.getByRole('button', { name: /Download selected \(1\)/i }); expect(downloadButton).toBeEnabled(); fireEvent.click(filename.closest('button')!); expect(screen.getByRole('button', { name: /Download selected \(0\)/i })).toBeDisabled(); await act(async () => { queryClient.setQueryData(['archive-printer-media', 1], { ...media, remote_files: [...media.remote_files], }); }); await waitFor(() => { expect(screen.getByRole('button', { name: /Download selected \(0\)/i })).toBeDisabled(); }); }); it('prunes a selected path that disappears during refetch', async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render( , ); expect(await screen.findByRole('button', { name: /Download selected \(1\)/i })).toBeEnabled(); await act(async () => { queryClient.setQueryData(['archive-printer-media', 1], { ...media, remote_files: [] }); }); await waitFor(() => expect(screen.queryByRole('button', { name: /Download selected/i })).not.toBeInTheDocument()); }); it('shows unavailable warnings even when no media was found', async () => { vi.mocked(api.getArchivePrinterMedia).mockResolvedValue({ ...media, remote_files: [], warnings: ['timelapse_unavailable', 'ipcam_unavailable'], }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render( , ); expect(await screen.findByText(/No timelapse or IP camera chunks were found/)).toBeInTheDocument(); expect(screen.getByText('The timelapse directory could not be read')).toBeInTheDocument(); expect(screen.getByText('The IP camera directory could not be read')).toBeInTheDocument(); }); it('shows how far the preparation has got', async () => { // The file browser has shown per-file progress from this same call all // along; without it the archive side is a spinner for as long as the // transfer takes, which for a few /ipcam chunks is minutes. vi.mocked(api.downloadPrinterFilesAsZip).mockImplementation( (_printerId, _paths, _sizes, _filename, _asZip, _signal, onProgress) => { onProgress?.(1, 2); return new Promise(() => {}); }, ); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render( , ); fireEvent.click(await screen.findByRole('button', { name: /Download selected \(1\)/i })); expect(await screen.findByText('1/2')).toBeInTheDocument(); }); it('cancels an in-flight printer preparation when the modal unmounts', async () => { let observedSignal: AbortSignal | undefined; vi.mocked(api.downloadPrinterFilesAsZip).mockImplementation( (_printerId, _paths, _sizes, _filename, _asZip, signal) => { observedSignal = signal; return new Promise((_resolve, reject) => { signal?.addEventListener('abort', () => reject(new DOMException('cancelled', 'AbortError'))); }); }, ); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const { unmount } = render( , ); fireEvent.click(await screen.findByRole('button', { name: /Download selected \(1\)/i })); await waitFor(() => expect(api.downloadPrinterFilesAsZip).toHaveBeenCalledOnce()); unmount(); expect(observedSignal?.aborted).toBe(true); }); it('reports a local timelapse token failure', async () => { vi.mocked(api.getArchivePrinterMedia).mockResolvedValue({ ...media, local_timelapse: { name: 'attached.mp4', size: 42 }, remote_files: [], }); vi.mocked(api.downloadArchiveTimelapse).mockRejectedValue(new Error('token expired')); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render( , ); fireEvent.click(await screen.findByRole('button', { name: /^Download$/i })); await waitFor(() => expect(showToast).toHaveBeenCalledWith( 'Download failed: token expired', 'error', )); }); });