SpreadsheetPreviewModal.test.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. /**
  2. * Tests for SpreadsheetPreviewModal (#2976).
  3. *
  4. * CSV parsing uses the real papaparse and XLSX parsing the real SheetJS —
  5. * only the network fetch is stubbed, so the tests cover the actual parse
  6. * paths the preview relies on.
  7. */
  8. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
  9. import { render, screen } from '@testing-library/react';
  10. import userEvent from '@testing-library/user-event';
  11. import * as XLSX from 'xlsx';
  12. import { SpreadsheetPreviewModal } from '../../components/SpreadsheetPreviewModal';
  13. vi.mock('../../api/client', () => ({
  14. api: {
  15. getLibraryFileDownloadUrl: vi.fn((id: number) => `http://test/library/files/${id}/download`),
  16. },
  17. getAuthToken: () => null,
  18. }));
  19. const mockOnClose = vi.fn();
  20. function stubFetchWith(bytes: ArrayBuffer | Uint8Array | string) {
  21. const body = typeof bytes === 'string' ? new TextEncoder().encode(bytes) : bytes;
  22. vi.stubGlobal(
  23. 'fetch',
  24. vi.fn(async () => new Response(body as BodyInit, { status: 200 })),
  25. );
  26. }
  27. function renderModal(props: Partial<Parameters<typeof SpreadsheetPreviewModal>[0]> = {}) {
  28. return render(
  29. <SpreadsheetPreviewModal
  30. libraryFileId={42}
  31. filename="parts.csv"
  32. fileType="csv"
  33. fileSize={1234}
  34. onClose={mockOnClose}
  35. {...props}
  36. />,
  37. );
  38. }
  39. describe('SpreadsheetPreviewModal', () => {
  40. beforeEach(() => {
  41. vi.clearAllMocks();
  42. });
  43. afterEach(() => {
  44. vi.unstubAllGlobals();
  45. });
  46. it('renders CSV cells as a read-only grid', async () => {
  47. stubFetchWith('Article,Qty\nM3 screw,12\nBearing 608,4\n');
  48. renderModal();
  49. expect(await screen.findByText('M3 screw')).toBeInTheDocument();
  50. expect(screen.getByText('Bearing 608')).toBeInTheDocument();
  51. expect(screen.getByText('Qty')).toBeInTheDocument();
  52. });
  53. it('shows a truncation notice for long CSV files', async () => {
  54. const rows = Array.from({ length: 600 }, (_, i) => `row${i},${i}`).join('\n');
  55. stubFetchWith(`name,value\n${rows}\n`);
  56. renderModal();
  57. expect(await screen.findByText('row0')).toBeInTheDocument();
  58. expect(screen.getByText(/Showing the first/)).toBeInTheDocument();
  59. expect(screen.queryByText('row599')).not.toBeInTheDocument();
  60. });
  61. it('renders XLSX workbooks with one tab per sheet', async () => {
  62. const workbook = XLSX.utils.book_new();
  63. XLSX.utils.book_append_sheet(
  64. workbook,
  65. XLSX.utils.aoa_to_sheet([
  66. ['Part', 'Price'],
  67. ['Nozzle', '12.50'],
  68. ]),
  69. 'Parts',
  70. );
  71. XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([['SupplierList']]), 'Suppliers');
  72. const bytes = XLSX.write(workbook, { type: 'array', bookType: 'xlsx' }) as ArrayBuffer;
  73. stubFetchWith(bytes);
  74. const user = userEvent.setup();
  75. renderModal({ filename: 'bom.xlsx', fileType: 'xlsx' });
  76. expect(await screen.findByText('Nozzle')).toBeInTheDocument();
  77. // Both sheets appear as tabs; switching shows the second sheet's content.
  78. await user.click(screen.getByRole('button', { name: 'Suppliers' }));
  79. expect(await screen.findByText('SupplierList')).toBeInTheDocument();
  80. expect(screen.queryByText('Nozzle')).not.toBeInTheDocument();
  81. });
  82. it('shows an error message for a broken workbook', async () => {
  83. // A truncated ZIP: SheetJS recognises the PK magic, then fails to parse.
  84. // (Plain text bytes would be leniently read as CSV, not rejected.)
  85. stubFetchWith(new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0x01, 0x02, 0x03]));
  86. renderModal({ filename: 'broken.xlsx', fileType: 'xlsx' });
  87. expect(await screen.findByText('This file cannot be previewed.')).toBeInTheDocument();
  88. });
  89. it('refuses oversized files without fetching them', async () => {
  90. const fetchSpy = vi.fn();
  91. vi.stubGlobal('fetch', fetchSpy);
  92. renderModal({ fileSize: 100 * 1024 * 1024 });
  93. expect(await screen.findByText(/too large to preview/)).toBeInTheDocument();
  94. expect(fetchSpy).not.toHaveBeenCalled();
  95. });
  96. });