import React from 'react'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import ExportDataModal from '../src/components/ExportDataModal'; import * as client from '../src/api/client'; jest.mock('../src/api/client', () => ({ experimentsApi: { getDailyStatuses: jest.fn() }, })); const animals = [ { id: 'a1', animal_name: 'Alpha', animal_id_string: 'R-001', subject_info: { grp: 'Control' } }, { id: 'a2', animal_name: 'Beta', animal_id_string: 'R-002', subject_info: { grp: 'Drug' } }, ]; const statuses = [ { animal_id: 'a1', date: '2026-07-01T00:00:00Z', analysis_summary: { total: 5, success_rate: 0.5 } }, { animal_id: 'a1', date: '2026-07-02T00:00:00Z', analysis_summary: { total: 7, success_rate: 0.7 } }, { animal_id: 'a2', date: '2026-07-01T00:00:00Z', analysis_summary: { total: 9, success_rate: 0.9 } }, ]; const subjectTemplate = [{ fieldId: 'grp', label: 'Group', active: true }]; beforeEach(() => { jest.clearAllMocks(); client.experimentsApi.getDailyStatuses.mockResolvedValue(statuses); }); // Intercept the Blob handed to URL.createObjectURL so tests can read the real CSV. // Also stub anchor.click() — jsdom treats a click on as an // unimplemented navigation that throws async and can flake a later test. function mockDownload() { let blob = null; const origCreate = global.URL.createObjectURL; const origRevoke = global.URL.revokeObjectURL; const origClick = window.HTMLAnchorElement.prototype.click; global.URL.createObjectURL = jest.fn((b) => { blob = b; return 'blob:mock'; }); global.URL.revokeObjectURL = jest.fn(); window.HTMLAnchorElement.prototype.click = jest.fn(); return { getBlob: () => blob, restore: async () => { await new Promise((r) => setTimeout(r, 0)); // flush handleExport's setTimeout revoke global.URL.createObjectURL = origCreate; global.URL.revokeObjectURL = origRevoke; window.HTMLAnchorElement.prototype.click = origClick; }, }; } function readBlobText(blob) { return new Promise((resolve, reject) => { const fr = new FileReader(); fr.onload = () => resolve(fr.result); fr.onerror = reject; fr.readAsText(blob); }); } function renderModal(props = {}) { return render( , ); } describe('ExportDataModal', () => { it('renders X / Data / Group by pickers; X defaults to Date', async () => { renderModal(); expect(await screen.findByLabelText('X axis (rows)')).toHaveValue('__date__'); expect(screen.getByLabelText('Data (cells)')).toBeInTheDocument(); expect(screen.getByLabelText('Group by')).toHaveValue('__none__'); await waitFor(() => expect(screen.getByText(/2 rows × 2 subjects/i)).toBeInTheDocument()); }); it('default export (X=Date, Data=Total attempts) has subject columns, date rows, blank where missing', async () => { const dl = mockDownload(); renderModal(); await screen.findByLabelText('X axis (rows)'); await waitFor(() => expect(screen.getByText(/2 rows/i)).toBeInTheDocument()); fireEvent.click(screen.getByText('Export CSV')); const text = await readBlobText(dl.getBlob()); expect(text).toBe('Data,Total attempts\n\nDate,Alpha,Beta\n2026-07-01,5,9\n2026-07-02,7,'); await dl.restore(); }); it('switching X to # Days Reach makes rows the day ordinals', async () => { const dl = mockDownload(); renderModal(); fireEvent.change(await screen.findByLabelText('X axis (rows)'), { target: { value: '__days__' } }); await waitFor(() => expect(screen.getByText(/2 rows/i)).toBeInTheDocument()); fireEvent.click(screen.getByText('Export CSV')); const text = await readBlobText(dl.getBlob()); expect(text).toBe('Data,Total attempts\n\n# Days Reach,Alpha,Beta\n1,5,9\n2,7,'); await dl.restore(); }); it('a valid group field adds a Group row and clusters subjects', async () => { const dl = mockDownload(); renderModal({ groupField: 'grp' }); await screen.findByLabelText('X axis (rows)'); await waitFor(() => expect(screen.getByText(/2 rows/i)).toBeInTheDocument()); fireEvent.click(screen.getByText('Export CSV')); const text = await readBlobText(dl.getBlob()); expect(text).toMatch(/^Group,Control,Drug$/m); await dl.restore(); }); it('a stale/unset group field coerces to None (no Group row in the export)', async () => { const dl = mockDownload(); renderModal({ groupField: 'ghost-field' }); await screen.findByLabelText('X axis (rows)'); await waitFor(() => expect(screen.getByText(/2 rows/i)).toBeInTheDocument()); fireEvent.click(screen.getByText('Export CSV')); const text = await readBlobText(dl.getBlob()); expect(text).not.toMatch(/^Group,/m); await dl.restore(); }); });