SpoolCsvImportModal.test.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. import { describe, it, expect, vi, beforeEach } from 'vitest';
  2. import { screen, waitFor, fireEvent } from '@testing-library/react';
  3. import { render } from '../utils';
  4. import { SpoolCsvImportModal } from '../../components/SpoolCsvImportModal';
  5. import { api, type CsvImportPreview } from '../../api/client';
  6. vi.mock('../../api/client', () => ({
  7. api: {
  8. importSpoolsCsvPreview: vi.fn(),
  9. importSpoolsCsv: vi.fn(),
  10. getSettings: vi.fn().mockResolvedValue({}),
  11. getAuthStatus: vi.fn().mockResolvedValue({ auth_enabled: false }),
  12. },
  13. }));
  14. const preview: CsvImportPreview = {
  15. columns: ['material', 'brand', 'color_name', 'rgba'],
  16. total: 3,
  17. valid_count: 2,
  18. error_count: 1,
  19. skipped_count: 0,
  20. warnings: [],
  21. rows: [
  22. { row_number: 1, status: 'valid', reason: null, material: 'PLA', brand: 'Polymaker', color_name: 'White', rgba: 'ffffffff', resolved_color: false, cross_material_color: false, duplicate_of_existing: false },
  23. { row_number: 2, status: 'error', reason: 'material is required', material: null, brand: 'Polymaker', color_name: 'X', rgba: null, resolved_color: false, cross_material_color: false, duplicate_of_existing: false },
  24. { row_number: 3, status: 'valid', reason: null, material: 'PETG', brand: 'Brand', color_name: 'Jade', rgba: 'e8e8e8ff', resolved_color: true, cross_material_color: true, duplicate_of_existing: false },
  25. ],
  26. };
  27. function selectFile() {
  28. const input = document.querySelector('input[type="file"]') as HTMLInputElement;
  29. const file = new File(['material\nPLA\n'], 'inventory.csv', { type: 'text/csv' });
  30. fireEvent.change(input, { target: { files: [file] } });
  31. return file;
  32. }
  33. describe('SpoolCsvImportModal', () => {
  34. beforeEach(() => {
  35. vi.clearAllMocks();
  36. });
  37. it('shows the preview table with per-row status after a file is chosen', async () => {
  38. vi.mocked(api.importSpoolsCsvPreview).mockResolvedValue(preview);
  39. render(<SpoolCsvImportModal onClose={vi.fn()} onImported={vi.fn()} />);
  40. selectFile();
  41. await waitFor(() => expect(api.importSpoolsCsvPreview).toHaveBeenCalledOnce());
  42. // Summary counts surfaced.
  43. expect(await screen.findByText('2 valid')).toBeInTheDocument();
  44. expect(screen.getByText('1 error')).toBeInTheDocument();
  45. // Error reason rendered inline.
  46. expect(screen.getByText('material is required')).toBeInTheDocument();
  47. // Import button reflects the valid count.
  48. expect(screen.getByText('Import 2 valid rows')).toBeInTheDocument();
  49. });
  50. it('imports only when there are valid rows and reports the created count', async () => {
  51. vi.mocked(api.importSpoolsCsvPreview).mockResolvedValue(preview);
  52. vi.mocked(api.importSpoolsCsv).mockResolvedValue({ created: 2, skipped: 0, errors: 1, error_rows: [] });
  53. const onImported = vi.fn();
  54. render(<SpoolCsvImportModal onClose={vi.fn()} onImported={onImported} />);
  55. selectFile();
  56. const importBtn = await screen.findByText('Import 2 valid rows');
  57. fireEvent.click(importBtn);
  58. await waitFor(() => expect(api.importSpoolsCsv).toHaveBeenCalledOnce());
  59. expect(onImported).toHaveBeenCalledWith(2);
  60. });
  61. it('disables import when no rows are valid', async () => {
  62. vi.mocked(api.importSpoolsCsvPreview).mockResolvedValue({
  63. ...preview,
  64. valid_count: 0,
  65. error_count: 1,
  66. rows: [preview.rows[1]],
  67. });
  68. render(<SpoolCsvImportModal onClose={vi.fn()} onImported={vi.fn()} />);
  69. selectFile();
  70. const noValid = await screen.findByText('No valid rows');
  71. expect(noValid.closest('button')).toBeDisabled();
  72. });
  73. });