125 lines
4.9 KiB
React
125 lines
4.9 KiB
React
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 <a href="blob:..."> 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(
|
||
<ExportDataModal
|
||
experimentId="exp-1"
|
||
experimentTitle="My Study"
|
||
dailyTemplate={[]}
|
||
animals={animals}
|
||
subjectTemplate={subjectTemplate}
|
||
groupField="__none__"
|
||
onClose={jest.fn()}
|
||
{...props}
|
||
/>,
|
||
);
|
||
}
|
||
|
||
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();
|
||
});
|
||
});
|