import React from 'react'; import { render, screen, fireEvent, act } from '@testing-library/react'; // recharts uses ResizeObserver (absent in jsdom); stub all used pieces as passthroughs. // Coverage boundary: with recharts mocked out, these tests cover only the sidebar // integration path (toggle, checkbox state, persistence). The legend onClick/hover // wiring, line hide/strokeWidth, and tooltip ordering are exercised by the pure-helper // unit tests (crossSubjectChart.test.js) and SubjectSidebar.test.jsx. jest.mock('recharts', () => { const Pass = ({ children }) =>
{children}
; return { ResponsiveContainer: Pass, LineChart: Pass, Line: () => null, XAxis: () => null, YAxis: () => null, CartesianGrid: () => null, Tooltip: () => null, Legend: () => null, }; }); import { ExperimentAnalysisCharts } from '../src/components/AnalysisCharts'; const ANIMALS = [ { id: 'a1', animal_name: 'Mouse-A1', subject_info: { grp: 'Group A' } }, { id: 'b1', animal_name: 'Mouse-B1', subject_info: { grp: 'Group B' } }, ]; const STATUSES = [ { animal_id: 'a1', date: '2026-01-01', analysis_summary: { total: 10, success_rate: 0.5, counts: { Success: 5 } } }, { animal_id: 'b1', date: '2026-01-01', analysis_summary: { total: 8, success_rate: 0.25, counts: { Success: 2 } } }, ]; const SUBJECT_TEMPLATE = [{ fieldId: 'grp', label: 'Group', active: true }]; const DAILY_TEMPLATE = []; function renderChart() { render( , ); } it('toggles the subject sidebar open and closed', () => { renderChart(); expect(screen.queryByText('Subjects')).toBeNull(); fireEvent.click(screen.getByText('▸ Subjects')); expect(screen.getByText('Subjects')).toBeInTheDocument(); expect(screen.getByLabelText('Mouse-A1')).toBeInTheDocument(); expect(screen.getByLabelText('Mouse-B1')).toBeInTheDocument(); fireEvent.click(screen.getByText('◂ Subjects')); expect(screen.queryByText('Subjects')).toBeNull(); }); it('unchecking a subject in the sidebar persists across re-render', () => { renderChart(); fireEvent.click(screen.getByText('▸ Subjects')); const cb = screen.getByLabelText('Mouse-A1'); expect(cb.checked).toBe(true); fireEvent.click(cb); expect(screen.getByLabelText('Mouse-A1').checked).toBe(false); expect(screen.getByText('Show all')).toBeInTheDocument(); }); it('hydrates hidden subjects and sidebar-open from plotConfig', () => { render( , ); expect(screen.getByLabelText('Mouse-A1').checked).toBe(false); expect(screen.getByLabelText('Mouse-B1').checked).toBe(true); }); describe('debounced auto-save', () => { beforeEach(() => jest.useFakeTimers()); afterEach(() => jest.useRealTimers()); it('does not call onPersistConfig on initial render', () => { const onPersist = jest.fn(); render( , ); act(() => { jest.advanceTimersByTime(1500); }); expect(onPersist).not.toHaveBeenCalled(); }); it('calls onPersistConfig ~1s after a change, with the updated config', () => { const onPersist = jest.fn().mockResolvedValue({}); render( , ); fireEvent.click(screen.getByLabelText('Mouse-A1')); expect(onPersist).not.toHaveBeenCalled(); act(() => { jest.advanceTimersByTime(1100); }); expect(onPersist).toHaveBeenCalledTimes(1); expect(onPersist.mock.calls[0][0].hiddenSubjects).toContain('a1'); }); it('does not save and clears the saving hint when a change is reverted before the debounce fires', () => { const onPersist = jest.fn().mockResolvedValue({}); render( , ); fireEvent.click(screen.getByLabelText('Mouse-A1')); // hide a1 expect(screen.getByText('Saving…')).toBeInTheDocument(); fireEvent.click(screen.getByLabelText('Mouse-A1')); // revert before the 1s timer act(() => { jest.advanceTimersByTime(1100); }); expect(onPersist).not.toHaveBeenCalled(); expect(screen.queryByText('Saving…')).toBeNull(); }); });