ArchiveMediaDownloadModal.test.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
  2. import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  3. import { beforeEach, describe, expect, it, vi } from 'vitest';
  4. import { ArchiveMediaDownloadModal } from '../../components/ArchiveMediaDownloadModal';
  5. import { api } from '../../api/client';
  6. const showToast = vi.fn();
  7. vi.mock('../../contexts/ToastContext', () => ({
  8. useToast: () => ({ showToast }),
  9. }));
  10. vi.mock('../../api/client', () => ({
  11. api: {
  12. getArchivePrinterMedia: vi.fn(),
  13. downloadArchiveTimelapse: vi.fn(),
  14. downloadPrinterFilesAsZip: vi.fn(),
  15. },
  16. }));
  17. const media = {
  18. archive_id: 1,
  19. printer_id: 1,
  20. local_timelapse: null,
  21. remote_files: [{
  22. name: 'video.mp4',
  23. path: '/timelapse/video.mp4',
  24. size: 1024,
  25. mtime: '2026-08-18T10:00:00Z',
  26. kind: 'timelapse' as const,
  27. }],
  28. warnings: [],
  29. };
  30. describe('ArchiveMediaDownloadModal', () => {
  31. beforeEach(() => {
  32. vi.clearAllMocks();
  33. vi.mocked(api.getArchivePrinterMedia).mockResolvedValue(media);
  34. });
  35. it('does not reselect a manually deselected single file when query data refreshes', async () => {
  36. const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  37. render(
  38. <QueryClientProvider client={queryClient}>
  39. <ArchiveMediaDownloadModal
  40. archiveId={1}
  41. archiveName="Test print"
  42. printerName="Printer"
  43. onClose={vi.fn()}
  44. />
  45. </QueryClientProvider>,
  46. );
  47. const filename = await screen.findByText('video.mp4');
  48. const downloadButton = screen.getByRole('button', { name: /Download selected \(1\)/i });
  49. expect(downloadButton).toBeEnabled();
  50. fireEvent.click(filename.closest('button')!);
  51. expect(screen.getByRole('button', { name: /Download selected \(0\)/i })).toBeDisabled();
  52. await act(async () => {
  53. queryClient.setQueryData(['archive-printer-media', 1], {
  54. ...media,
  55. remote_files: [...media.remote_files],
  56. });
  57. });
  58. await waitFor(() => {
  59. expect(screen.getByRole('button', { name: /Download selected \(0\)/i })).toBeDisabled();
  60. });
  61. });
  62. it('prunes a selected path that disappears during refetch', async () => {
  63. const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  64. render(
  65. <QueryClientProvider client={queryClient}>
  66. <ArchiveMediaDownloadModal
  67. archiveId={1}
  68. archiveName="Test print"
  69. printerName="Printer"
  70. onClose={vi.fn()}
  71. />
  72. </QueryClientProvider>,
  73. );
  74. expect(await screen.findByRole('button', { name: /Download selected \(1\)/i })).toBeEnabled();
  75. await act(async () => {
  76. queryClient.setQueryData(['archive-printer-media', 1], { ...media, remote_files: [] });
  77. });
  78. await waitFor(() => expect(screen.queryByRole('button', { name: /Download selected/i })).not.toBeInTheDocument());
  79. });
  80. it('shows unavailable warnings even when no media was found', async () => {
  81. vi.mocked(api.getArchivePrinterMedia).mockResolvedValue({
  82. ...media,
  83. remote_files: [],
  84. warnings: ['timelapse_unavailable', 'ipcam_unavailable'],
  85. });
  86. const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  87. render(
  88. <QueryClientProvider client={queryClient}>
  89. <ArchiveMediaDownloadModal
  90. archiveId={1}
  91. archiveName="Test print"
  92. printerName="Printer"
  93. onClose={vi.fn()}
  94. />
  95. </QueryClientProvider>,
  96. );
  97. expect(await screen.findByText(/No timelapse or IP camera chunks were found/)).toBeInTheDocument();
  98. expect(screen.getByText('The timelapse directory could not be read')).toBeInTheDocument();
  99. expect(screen.getByText('The IP camera directory could not be read')).toBeInTheDocument();
  100. });
  101. it('shows how far the preparation has got', async () => {
  102. // The file browser has shown per-file progress from this same call all
  103. // along; without it the archive side is a spinner for as long as the
  104. // transfer takes, which for a few /ipcam chunks is minutes.
  105. vi.mocked(api.downloadPrinterFilesAsZip).mockImplementation(
  106. (_printerId, _paths, _sizes, _filename, _asZip, _signal, onProgress) => {
  107. onProgress?.(1, 2);
  108. return new Promise(() => {});
  109. },
  110. );
  111. const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  112. render(
  113. <QueryClientProvider client={queryClient}>
  114. <ArchiveMediaDownloadModal
  115. archiveId={1}
  116. archiveName="Test print"
  117. printerName="Printer"
  118. onClose={vi.fn()}
  119. />
  120. </QueryClientProvider>,
  121. );
  122. fireEvent.click(await screen.findByRole('button', { name: /Download selected \(1\)/i }));
  123. expect(await screen.findByText('1/2')).toBeInTheDocument();
  124. });
  125. it('cancels an in-flight printer preparation when the modal unmounts', async () => {
  126. let observedSignal: AbortSignal | undefined;
  127. vi.mocked(api.downloadPrinterFilesAsZip).mockImplementation(
  128. (_printerId, _paths, _sizes, _filename, _asZip, signal) => {
  129. observedSignal = signal;
  130. return new Promise((_resolve, reject) => {
  131. signal?.addEventListener('abort', () => reject(new DOMException('cancelled', 'AbortError')));
  132. });
  133. },
  134. );
  135. const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  136. const { unmount } = render(
  137. <QueryClientProvider client={queryClient}>
  138. <ArchiveMediaDownloadModal
  139. archiveId={1}
  140. archiveName="Test print"
  141. printerName="Printer"
  142. onClose={vi.fn()}
  143. />
  144. </QueryClientProvider>,
  145. );
  146. fireEvent.click(await screen.findByRole('button', { name: /Download selected \(1\)/i }));
  147. await waitFor(() => expect(api.downloadPrinterFilesAsZip).toHaveBeenCalledOnce());
  148. unmount();
  149. expect(observedSignal?.aborted).toBe(true);
  150. });
  151. it('reports a local timelapse token failure', async () => {
  152. vi.mocked(api.getArchivePrinterMedia).mockResolvedValue({
  153. ...media,
  154. local_timelapse: { name: 'attached.mp4', size: 42 },
  155. remote_files: [],
  156. });
  157. vi.mocked(api.downloadArchiveTimelapse).mockRejectedValue(new Error('token expired'));
  158. const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  159. render(
  160. <QueryClientProvider client={queryClient}>
  161. <ArchiveMediaDownloadModal
  162. archiveId={1}
  163. archiveName="Test print"
  164. printerName="Printer"
  165. onClose={vi.fn()}
  166. />
  167. </QueryClientProvider>,
  168. );
  169. fireEvent.click(await screen.findByRole('button', { name: /^Download$/i }));
  170. await waitFor(() => expect(showToast).toHaveBeenCalledWith(
  171. 'Download failed: token expired',
  172. 'error',
  173. ));
  174. });
  175. });