FileUploadModal.test.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. /**
  2. * Tests for the FileUploadModal component.
  3. * Tests file upload, drag-and-drop, ZIP/3MF/STL detection, and autoUpload mode.
  4. */
  5. import { describe, it, expect, vi, beforeEach } from 'vitest';
  6. import { screen, fireEvent, waitFor } from '@testing-library/react';
  7. import userEvent from '@testing-library/user-event';
  8. import { render } from '../utils';
  9. import { FileUploadModal } from '../../components/FileUploadModal';
  10. import { http, HttpResponse } from 'msw';
  11. import { server } from '../mocks/server';
  12. describe('FileUploadModal', () => {
  13. const defaultProps = {
  14. folderId: null as number | null,
  15. onClose: vi.fn(),
  16. onUploadComplete: vi.fn(),
  17. };
  18. beforeEach(() => {
  19. vi.clearAllMocks();
  20. server.use(
  21. http.post('/api/v1/library/files', () => {
  22. return HttpResponse.json({
  23. id: 1,
  24. filename: 'test.gcode.3mf',
  25. file_type: '3mf',
  26. file_size: 1048576,
  27. thumbnail_path: null,
  28. duplicate_of: null,
  29. metadata: null,
  30. });
  31. }),
  32. http.post('/api/v1/library/extract-zip', () => {
  33. return HttpResponse.json({
  34. extracted: 3,
  35. errors: [],
  36. });
  37. })
  38. );
  39. });
  40. describe('rendering', () => {
  41. it('renders the modal with title', () => {
  42. render(<FileUploadModal {...defaultProps} />);
  43. expect(screen.getByText('Upload Files')).toBeInTheDocument();
  44. });
  45. it('renders drag and drop zone', () => {
  46. render(<FileUploadModal {...defaultProps} />);
  47. expect(screen.getByText(/Drag & drop/)).toBeInTheDocument();
  48. });
  49. it('renders click to browse text', () => {
  50. render(<FileUploadModal {...defaultProps} />);
  51. expect(screen.getByText(/click to browse/i)).toBeInTheDocument();
  52. });
  53. it('renders Cancel button', () => {
  54. render(<FileUploadModal {...defaultProps} />);
  55. expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
  56. });
  57. it('renders Upload button disabled when no files', () => {
  58. render(<FileUploadModal {...defaultProps} />);
  59. const uploadButton = screen.getByRole('button', { name: /Upload/i });
  60. expect(uploadButton).toBeDisabled();
  61. });
  62. it('shows all file types supported text', () => {
  63. render(<FileUploadModal {...defaultProps} />);
  64. expect(screen.getByText(/All file types supported/i)).toBeInTheDocument();
  65. });
  66. });
  67. describe('file selection', () => {
  68. it('shows added file in the list', async () => {
  69. const user = userEvent.setup();
  70. render(<FileUploadModal {...defaultProps} />);
  71. const file = new File(['content'], 'model.gcode.3mf', { type: 'application/octet-stream' });
  72. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  73. await user.upload(fileInput, file);
  74. expect(screen.getByText('model.gcode.3mf')).toBeInTheDocument();
  75. });
  76. it('shows file size in MB', async () => {
  77. const user = userEvent.setup();
  78. render(<FileUploadModal {...defaultProps} />);
  79. const file = new File(['x'.repeat(1048576)], 'model.3mf', { type: 'application/octet-stream' });
  80. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  81. await user.upload(fileInput, file);
  82. expect(screen.getByText('1.00 MB')).toBeInTheDocument();
  83. });
  84. it('enables Upload button when files are added', async () => {
  85. const user = userEvent.setup();
  86. render(<FileUploadModal {...defaultProps} />);
  87. const file = new File(['content'], 'model.3mf', { type: 'application/octet-stream' });
  88. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  89. await user.upload(fileInput, file);
  90. const uploadButton = screen.getByRole('button', { name: /Upload \(1\)/i });
  91. expect(uploadButton).not.toBeDisabled();
  92. });
  93. it('shows file count in Upload button', async () => {
  94. const user = userEvent.setup();
  95. render(<FileUploadModal {...defaultProps} />);
  96. const files = [
  97. new File(['a'], 'file1.3mf', { type: 'application/octet-stream' }),
  98. new File(['b'], 'file2.stl', { type: 'application/octet-stream' }),
  99. ];
  100. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  101. await user.upload(fileInput, files);
  102. expect(screen.getByRole('button', { name: /Upload \(2\)/i })).toBeInTheDocument();
  103. });
  104. it('accepts any file type (not restricted like UploadModal)', async () => {
  105. const user = userEvent.setup();
  106. render(<FileUploadModal {...defaultProps} />);
  107. const file = new File(['content'], 'readme.txt', { type: 'text/plain' });
  108. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  109. await user.upload(fileInput, file);
  110. expect(screen.getByText('readme.txt')).toBeInTheDocument();
  111. });
  112. });
  113. describe('file removal', () => {
  114. it('removes a file when X button is clicked', async () => {
  115. const user = userEvent.setup();
  116. render(<FileUploadModal {...defaultProps} />);
  117. const file = new File(['content'], 'model.3mf', { type: 'application/octet-stream' });
  118. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  119. await user.upload(fileInput, file);
  120. expect(screen.getByText('model.3mf')).toBeInTheDocument();
  121. const fileRow = screen.getByText('model.3mf').closest('.flex');
  122. const removeButton = fileRow?.querySelector('button');
  123. if (removeButton) {
  124. await user.click(removeButton);
  125. }
  126. await waitFor(() => {
  127. expect(screen.queryByText('model.3mf')).not.toBeInTheDocument();
  128. });
  129. });
  130. it('disables Upload button after removing all files', async () => {
  131. const user = userEvent.setup();
  132. render(<FileUploadModal {...defaultProps} />);
  133. const file = new File(['content'], 'model.3mf', { type: 'application/octet-stream' });
  134. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  135. await user.upload(fileInput, file);
  136. const fileRow = screen.getByText('model.3mf').closest('.flex');
  137. const removeButton = fileRow?.querySelector('button');
  138. if (removeButton) {
  139. await user.click(removeButton);
  140. }
  141. await waitFor(() => {
  142. const uploadButton = screen.getByRole('button', { name: /Upload/i });
  143. expect(uploadButton).toBeDisabled();
  144. });
  145. });
  146. });
  147. describe('file type detection', () => {
  148. it('shows ZIP options when .zip file is added', async () => {
  149. const user = userEvent.setup();
  150. render(<FileUploadModal {...defaultProps} />);
  151. const zipFile = new File(['pk'], 'models.zip', { type: 'application/zip' });
  152. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  153. await user.upload(fileInput, zipFile);
  154. await waitFor(() => {
  155. expect(screen.getByText('ZIP files detected')).toBeInTheDocument();
  156. expect(screen.getByText(/Preserve folder structure/)).toBeInTheDocument();
  157. expect(screen.getByText(/Create folder from ZIP/)).toBeInTheDocument();
  158. });
  159. });
  160. it('shows 3MF info when .3mf file is added', async () => {
  161. const user = userEvent.setup();
  162. render(<FileUploadModal {...defaultProps} />);
  163. const threemfFile = new File(['content'], 'model.gcode.3mf', { type: 'application/octet-stream' });
  164. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  165. await user.upload(fileInput, threemfFile);
  166. await waitFor(() => {
  167. expect(screen.getByText('3MF files detected')).toBeInTheDocument();
  168. });
  169. });
  170. it('shows STL thumbnail option when .stl file is added', async () => {
  171. const user = userEvent.setup();
  172. render(<FileUploadModal {...defaultProps} />);
  173. const stlFile = new File(['solid'], 'bracket.stl', { type: 'application/sla' });
  174. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  175. await user.upload(fileInput, stlFile);
  176. await waitFor(() => {
  177. expect(screen.getByText('STL thumbnail generation')).toBeInTheDocument();
  178. expect(screen.getByText(/Thumbnails can be generated/i)).toBeInTheDocument();
  179. });
  180. });
  181. it('shows STL thumbnail option when ZIP file is added (may contain STLs)', async () => {
  182. const user = userEvent.setup();
  183. render(<FileUploadModal {...defaultProps} />);
  184. const zipFile = new File(['pk'], 'models.zip', { type: 'application/zip' });
  185. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  186. await user.upload(fileInput, zipFile);
  187. await waitFor(() => {
  188. expect(screen.getByText('STL thumbnail generation')).toBeInTheDocument();
  189. expect(screen.getByText(/ZIP files may contain STL/i)).toBeInTheDocument();
  190. });
  191. });
  192. });
  193. describe('ZIP options', () => {
  194. it('preserve structure checkbox is checked by default', async () => {
  195. const user = userEvent.setup();
  196. render(<FileUploadModal {...defaultProps} />);
  197. const zipFile = new File(['pk'], 'models.zip', { type: 'application/zip' });
  198. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  199. await user.upload(fileInput, zipFile);
  200. await waitFor(() => {
  201. const label = screen.getByText(/Preserve folder structure/).closest('label');
  202. const checkbox = label?.querySelector('input[type="checkbox"]') as HTMLInputElement;
  203. expect(checkbox).toBeChecked();
  204. });
  205. });
  206. it('create folder checkbox is unchecked by default', async () => {
  207. const user = userEvent.setup();
  208. render(<FileUploadModal {...defaultProps} />);
  209. const zipFile = new File(['pk'], 'models.zip', { type: 'application/zip' });
  210. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  211. await user.upload(fileInput, zipFile);
  212. await waitFor(() => {
  213. const label = screen.getByText(/Create folder from ZIP/).closest('label');
  214. const checkbox = label?.querySelector('input[type="checkbox"]') as HTMLInputElement;
  215. expect(checkbox).not.toBeChecked();
  216. });
  217. });
  218. it('can toggle ZIP options', async () => {
  219. const user = userEvent.setup();
  220. render(<FileUploadModal {...defaultProps} />);
  221. const zipFile = new File(['pk'], 'models.zip', { type: 'application/zip' });
  222. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  223. await user.upload(fileInput, zipFile);
  224. await waitFor(() => {
  225. expect(screen.getByText('ZIP files detected')).toBeInTheDocument();
  226. });
  227. const preserveLabel = screen.getByText(/Preserve folder structure/).closest('label');
  228. const preserveCheckbox = preserveLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement;
  229. await user.click(preserveCheckbox);
  230. expect(preserveCheckbox).not.toBeChecked();
  231. const createFolderLabel = screen.getByText(/Create folder from ZIP/).closest('label');
  232. const createFolderCheckbox = createFolderLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement;
  233. await user.click(createFolderCheckbox);
  234. expect(createFolderCheckbox).toBeChecked();
  235. });
  236. });
  237. describe('upload flow', () => {
  238. it('calls onUploadComplete after successful upload', async () => {
  239. const user = userEvent.setup();
  240. render(<FileUploadModal {...defaultProps} />);
  241. const file = new File(['content'], 'model.3mf', { type: 'application/octet-stream' });
  242. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  243. await user.upload(fileInput, file);
  244. const uploadButton = screen.getByRole('button', { name: /Upload \(1\)/i });
  245. await user.click(uploadButton);
  246. await waitFor(() => {
  247. expect(defaultProps.onUploadComplete).toHaveBeenCalled();
  248. });
  249. });
  250. it('calls onFileUploaded with response data for each file', async () => {
  251. const onFileUploaded = vi.fn();
  252. const user = userEvent.setup();
  253. render(<FileUploadModal {...defaultProps} onFileUploaded={onFileUploaded} />);
  254. const file = new File(['content'], 'model.3mf', { type: 'application/octet-stream' });
  255. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  256. await user.upload(fileInput, file);
  257. const uploadButton = screen.getByRole('button', { name: /Upload \(1\)/i });
  258. await user.click(uploadButton);
  259. await waitFor(() => {
  260. expect(onFileUploaded).toHaveBeenCalledWith(
  261. expect.objectContaining({
  262. id: 1,
  263. filename: 'test.gcode.3mf',
  264. })
  265. );
  266. });
  267. });
  268. it('shows uploading state while uploading', async () => {
  269. // Delay the response to observe uploading state
  270. server.use(
  271. http.post('/api/v1/library/files', async () => {
  272. await new Promise((resolve) => setTimeout(resolve, 100));
  273. return HttpResponse.json({
  274. id: 1,
  275. filename: 'model.3mf',
  276. file_type: '3mf',
  277. file_size: 1024,
  278. thumbnail_path: null,
  279. duplicate_of: null,
  280. metadata: null,
  281. });
  282. })
  283. );
  284. const user = userEvent.setup();
  285. render(<FileUploadModal {...defaultProps} />);
  286. const file = new File(['content'], 'model.3mf', { type: 'application/octet-stream' });
  287. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  288. await user.upload(fileInput, file);
  289. const uploadButton = screen.getByRole('button', { name: /Upload \(1\)/i });
  290. await user.click(uploadButton);
  291. // Should show uploading state
  292. await waitFor(() => {
  293. expect(screen.getByText('Uploading...')).toBeInTheDocument();
  294. expect(document.querySelector('.animate-spin')).toBeInTheDocument();
  295. });
  296. });
  297. it('shows error state on upload failure', async () => {
  298. server.use(
  299. http.post('/api/v1/library/files', () => {
  300. return HttpResponse.json({ detail: 'File too large' }, { status: 413 });
  301. })
  302. );
  303. const user = userEvent.setup();
  304. render(<FileUploadModal {...defaultProps} />);
  305. const file = new File(['content'], 'model.3mf', { type: 'application/octet-stream' });
  306. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  307. await user.upload(fileInput, file);
  308. const uploadButton = screen.getByRole('button', { name: /Upload \(1\)/i });
  309. await user.click(uploadButton);
  310. await waitFor(() => {
  311. expect(defaultProps.onUploadComplete).toHaveBeenCalled();
  312. });
  313. });
  314. it('closes modal after manual upload completes', async () => {
  315. const user = userEvent.setup();
  316. render(<FileUploadModal {...defaultProps} />);
  317. const file = new File(['content'], 'model.3mf', { type: 'application/octet-stream' });
  318. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  319. await user.upload(fileInput, file);
  320. const uploadButton = screen.getByRole('button', { name: /Upload \(1\)/i });
  321. await user.click(uploadButton);
  322. await waitFor(() => {
  323. expect(defaultProps.onUploadComplete).toHaveBeenCalled();
  324. expect(defaultProps.onClose).toHaveBeenCalled();
  325. });
  326. });
  327. });
  328. describe('autoUpload mode', () => {
  329. it('uploads immediately when file is added', async () => {
  330. const onFileUploaded = vi.fn();
  331. const user = userEvent.setup();
  332. render(
  333. <FileUploadModal
  334. {...defaultProps}
  335. autoUpload
  336. onFileUploaded={onFileUploaded}
  337. />
  338. );
  339. const file = new File(['content'], 'model.gcode.3mf', { type: 'application/octet-stream' });
  340. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  341. await user.upload(fileInput, file);
  342. await waitFor(() => {
  343. expect(onFileUploaded).toHaveBeenCalledWith(
  344. expect.objectContaining({ id: 1 })
  345. );
  346. });
  347. });
  348. it('calls onClose after autoUpload completes', async () => {
  349. const user = userEvent.setup();
  350. render(<FileUploadModal {...defaultProps} autoUpload />);
  351. const file = new File(['content'], 'model.gcode.3mf', { type: 'application/octet-stream' });
  352. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  353. await user.upload(fileInput, file);
  354. await waitFor(() => {
  355. expect(defaultProps.onClose).toHaveBeenCalled();
  356. expect(defaultProps.onUploadComplete).toHaveBeenCalled();
  357. });
  358. });
  359. });
  360. describe('close behavior', () => {
  361. it('calls onClose when Cancel button is clicked', async () => {
  362. const user = userEvent.setup();
  363. render(<FileUploadModal {...defaultProps} />);
  364. await user.click(screen.getByRole('button', { name: 'Cancel' }));
  365. expect(defaultProps.onClose).toHaveBeenCalled();
  366. });
  367. it('calls onClose when X button is clicked', async () => {
  368. const user = userEvent.setup();
  369. render(<FileUploadModal {...defaultProps} />);
  370. // The X button is the one in the header (not file remove buttons)
  371. const headerButtons = screen.getByText('Upload Files').parentElement?.querySelectorAll('button');
  372. const closeButton = headerButtons?.[0];
  373. if (closeButton) {
  374. await user.click(closeButton);
  375. expect(defaultProps.onClose).toHaveBeenCalled();
  376. }
  377. });
  378. it('always shows Cancel button (modal auto-closes after upload)', () => {
  379. render(<FileUploadModal {...defaultProps} />);
  380. expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
  381. });
  382. });
  383. describe('drag and drop', () => {
  384. it('highlights drop zone on drag over', () => {
  385. render(<FileUploadModal {...defaultProps} />);
  386. const dropZone = screen.getByText(/Drag & drop/).closest('div[class*="border-dashed"]');
  387. if (dropZone) {
  388. fireEvent.dragOver(dropZone, { dataTransfer: { files: [] } });
  389. expect(dropZone.className).toContain('border-bambu-green');
  390. }
  391. });
  392. it('removes highlight on drag leave', () => {
  393. render(<FileUploadModal {...defaultProps} />);
  394. const dropZone = screen.getByText(/Drag & drop/).closest('div[class*="border-dashed"]');
  395. if (dropZone) {
  396. fireEvent.dragOver(dropZone, { dataTransfer: { files: [] } });
  397. fireEvent.dragLeave(dropZone, { dataTransfer: { files: [] } });
  398. expect(dropZone.className).not.toContain('bg-bambu-green');
  399. }
  400. });
  401. });
  402. describe('folder context', () => {
  403. it('accepts folderId prop for uploading to specific folder', () => {
  404. render(<FileUploadModal {...defaultProps} folderId={5} />);
  405. // Component should render without errors with a folder context
  406. expect(screen.getByText('Upload Files')).toBeInTheDocument();
  407. });
  408. });
  409. describe('validateFile prop', () => {
  410. it('rejects files that fail validation and shows error', async () => {
  411. const user = userEvent.setup();
  412. render(
  413. <FileUploadModal
  414. {...defaultProps}
  415. validateFile={(file) => {
  416. if (!file.name.endsWith('.gcode')) return 'Only .gcode files allowed';
  417. }}
  418. />
  419. );
  420. const file = new File(['content'], 'model.stl', { type: 'application/octet-stream' });
  421. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  422. await user.upload(fileInput, file);
  423. // Error should be shown
  424. expect(screen.getByText('Only .gcode files allowed')).toBeInTheDocument();
  425. // File should NOT be added to the list
  426. expect(screen.queryByText('model.stl')).not.toBeInTheDocument();
  427. });
  428. it('allows files that pass validation', async () => {
  429. const user = userEvent.setup();
  430. render(
  431. <FileUploadModal
  432. {...defaultProps}
  433. validateFile={(file) => {
  434. if (!file.name.endsWith('.gcode')) return 'Only .gcode files allowed';
  435. }}
  436. />
  437. );
  438. const file = new File(['content'], 'model.gcode', { type: 'application/octet-stream' });
  439. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  440. await user.upload(fileInput, file);
  441. expect(screen.getByText('model.gcode')).toBeInTheDocument();
  442. expect(screen.queryByText('Only .gcode files allowed')).not.toBeInTheDocument();
  443. });
  444. it('clears validation error when a new file is added', async () => {
  445. const user = userEvent.setup();
  446. render(
  447. <FileUploadModal
  448. {...defaultProps}
  449. validateFile={(file) => {
  450. if (!file.name.endsWith('.gcode')) return 'Only .gcode files allowed';
  451. }}
  452. />
  453. );
  454. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  455. // First add an invalid file
  456. const badFile = new File(['content'], 'model.stl', { type: 'application/octet-stream' });
  457. await user.upload(fileInput, badFile);
  458. expect(screen.getByText('Only .gcode files allowed')).toBeInTheDocument();
  459. // Then add a valid file — error should clear
  460. const goodFile = new File(['content'], 'model.gcode', { type: 'application/octet-stream' });
  461. await user.upload(fileInput, goodFile);
  462. expect(screen.queryByText('Only .gcode files allowed')).not.toBeInTheDocument();
  463. });
  464. });
  465. describe('accept prop', () => {
  466. it('sets accept attribute on file input', () => {
  467. render(<FileUploadModal {...defaultProps} accept=".gcode,.gcode.3mf" />);
  468. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  469. expect(fileInput.accept).toBe('.gcode,.gcode.3mf');
  470. });
  471. it('does not set accept attribute when prop is omitted', () => {
  472. render(<FileUploadModal {...defaultProps} />);
  473. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  474. expect(fileInput.accept).toBe('');
  475. });
  476. });
  477. describe('onFileUploaded error handling', () => {
  478. it('shows error and keeps modal open when onFileUploaded returns a string', async () => {
  479. const user = userEvent.setup();
  480. render(
  481. <FileUploadModal
  482. {...defaultProps}
  483. onFileUploaded={() => 'This file was sliced for the wrong printer'}
  484. />
  485. );
  486. const file = new File(['content'], 'model.3mf', { type: 'application/octet-stream' });
  487. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  488. await user.upload(fileInput, file);
  489. const uploadButton = screen.getByRole('button', { name: /Upload \(1\)/i });
  490. await user.click(uploadButton);
  491. await waitFor(() => {
  492. expect(screen.getByText('This file was sliced for the wrong printer')).toBeInTheDocument();
  493. });
  494. // Modal should NOT close
  495. expect(defaultProps.onClose).not.toHaveBeenCalled();
  496. });
  497. it('clears file list when onFileUploaded returns an error', async () => {
  498. const user = userEvent.setup();
  499. render(
  500. <FileUploadModal
  501. {...defaultProps}
  502. onFileUploaded={() => 'Incompatible printer'}
  503. />
  504. );
  505. const file = new File(['content'], 'model.3mf', { type: 'application/octet-stream' });
  506. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  507. await user.upload(fileInput, file);
  508. const uploadButton = screen.getByRole('button', { name: /Upload \(1\)/i });
  509. await user.click(uploadButton);
  510. await waitFor(() => {
  511. expect(screen.getByText('Incompatible printer')).toBeInTheDocument();
  512. });
  513. // File list should be cleared
  514. expect(screen.queryByText('model.3mf')).not.toBeInTheDocument();
  515. });
  516. it('closes modal normally when onFileUploaded returns undefined', async () => {
  517. const onFileUploaded = vi.fn();
  518. const user = userEvent.setup();
  519. render(<FileUploadModal {...defaultProps} onFileUploaded={onFileUploaded} />);
  520. const file = new File(['content'], 'model.3mf', { type: 'application/octet-stream' });
  521. const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  522. await user.upload(fileInput, file);
  523. const uploadButton = screen.getByRole('button', { name: /Upload \(1\)/i });
  524. await user.click(uploadButton);
  525. await waitFor(() => {
  526. expect(defaultProps.onClose).toHaveBeenCalled();
  527. });
  528. });
  529. });
  530. });