feat(frontend): hydrate + debounced-persist cross-subject plot config

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Experiments DB Dev
2026-06-01 10:22:39 -04:00
parent 4b82df514f
commit 9a9b5dfae8
4 changed files with 143 additions and 29 deletions
@@ -1,5 +1,5 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/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
@@ -59,3 +59,68 @@ it('unchecking a subject in the sidebar persists across re-render', () => {
expect(screen.getByLabelText('Mouse-A1').checked).toBe(false);
expect(screen.getByText('Show all')).toBeInTheDocument();
});
it('hydrates hidden subjects and sidebar-open from plotConfig', () => {
render(
<ExperimentAnalysisCharts
animals={ANIMALS}
experimentStatuses={STATUSES}
subjectTemplate={SUBJECT_TEMPLATE}
dailyTemplate={DAILY_TEMPLATE}
plotConfig={{ hiddenSubjects: ['a1'], sidebarOpen: true }}
/>,
);
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(
<ExperimentAnalysisCharts
animals={ANIMALS} experimentStatuses={STATUSES}
subjectTemplate={SUBJECT_TEMPLATE} dailyTemplate={DAILY_TEMPLATE}
plotConfig={null} onPersistConfig={onPersist}
/>,
);
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(
<ExperimentAnalysisCharts
animals={ANIMALS} experimentStatuses={STATUSES}
subjectTemplate={SUBJECT_TEMPLATE} dailyTemplate={DAILY_TEMPLATE}
plotConfig={{ sidebarOpen: true }} onPersistConfig={onPersist}
/>,
);
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(
<ExperimentAnalysisCharts
animals={ANIMALS} experimentStatuses={STATUSES}
subjectTemplate={SUBJECT_TEMPLATE} dailyTemplate={DAILY_TEMPLATE}
plotConfig={{ sidebarOpen: true }} onPersistConfig={onPersist}
/>,
);
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();
});
});