test: document and test analysis_summary bar chart contract

5 tests cover the bar chart rules:
- Shows when analysis_summary.total > 0
- Hidden when no summary or total=0
- Multiple animals each with their own summary
- Animals excluded from day view (empty field) also excluded from chart
- animalsWithData now filters by selected field value (not just record presence)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Experiments DB Dev
2026-04-23 12:31:36 -04:00
parent 5f3ad1d408
commit 6ecc6dc19a
2 changed files with 71 additions and 10 deletions
@@ -48,8 +48,15 @@ function DayPanel({ date, animals, allStatuses, template, experimentId, selected
return map; return map;
}, [allStatuses, dateStr]); }, [allStatuses, dateStr]);
// Only show animals that have a status record for this day // Only show animals that have a status record for this day WITH a non-empty selected field
const animalsWithData = animals.filter((a) => statusByAnimal[a.id]); const animalsWithData = animals.filter((a) => {
const s = statusByAnimal[a.id];
if (!s) return false;
if (!selectedField) return true;
const isBuiltin = BUILTIN_KEYS_SET.has(selectedField);
const val = isBuiltin ? s[selectedField] : s.custom_fields?.[selectedField];
return val !== null && val !== undefined && String(val).trim() !== '';
});
const fieldLabel = fieldOptions.find((f) => f.value === selectedField)?.label; const fieldLabel = fieldOptions.find((f) => f.value === selectedField)?.label;
+62 -8
View File
@@ -247,33 +247,87 @@ describe('day click modal', () => {
}); });
// ── Bar chart (inside day modal) ────────────────────────────────────────────── // ── Bar chart (inside day modal) ──────────────────────────────────────────────
//
// The bar chart reads from status.analysis_summary — a pre-computed field on
// each DailyStatus record containing trial-level session metrics:
// { total: number, counts: { Success: number, Failure: number }, success_rate: number }
//
// X-axis: animals that have a valid (non-empty) value for the chosen calendar
// field on THAT specific day. Each day can show a different set of animals.
// Bars: Total / Success / Failure from analysis_summary for that animal on that day.
//
// Chart is hidden when no animal on that day has analysis_summary.total > 0.
// Do NOT compute success/failure from the field value itself — always use analysis_summary.
describe('bar chart inside day modal', () => { describe('bar chart inside day modal', () => {
it('renders session metrics bar chart when analysis_summary has data', async () => { it('renders session metrics bar chart when at least one animal has analysis_summary', async () => {
const dateStr = todayStr(); const dateStr = todayStr();
const summary = { total: 18, counts: { Success: 5, Failure: 13 }, success_rate: 0.28 }; const summary = { total: 18, counts: { Success: 5, Failure: 13 }, success_rate: 0.28 };
const statuses = [ const statuses = [
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }, null, summary), makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }, null, summary),
]; ];
render( render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />);
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />,
);
fireEvent.click(screen.getByTestId(`day-${dateStr}`)); fireEvent.click(screen.getByTestId(`day-${dateStr}`));
await waitFor(() => { await waitFor(() => {
expect(within(screen.getByRole('dialog')).getByTestId('bar-chart')).toBeInTheDocument(); expect(within(screen.getByRole('dialog')).getByTestId('bar-chart')).toBeInTheDocument();
}); });
}); });
it('hides bar chart when no animal has analysis_summary with total > 0', async () => { it('hides bar chart when no animal has analysis_summary (null or total=0)', async () => {
const dateStr = todayStr(); const dateStr = todayStr();
const statuses = [ const statuses = [
// Has field data (so day is clickable) but no analysis_summary
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }), makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }),
]; ];
render( render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />);
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />,
);
fireEvent.click(screen.getByTestId(`day-${dateStr}`)); fireEvent.click(screen.getByTestId(`day-${dateStr}`));
await waitFor(() => screen.getByRole('dialog')); await waitFor(() => screen.getByRole('dialog'));
expect(screen.queryByTestId('bar-chart')).not.toBeInTheDocument(); expect(screen.queryByTestId('bar-chart')).not.toBeInTheDocument();
}); });
it('hides bar chart when analysis_summary exists but total is 0', async () => {
const dateStr = todayStr();
const emptySummary = { total: 0, counts: { Success: 0, Failure: 0 }, success_rate: null };
const statuses = [
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }, null, emptySummary),
];
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />);
fireEvent.click(screen.getByTestId(`day-${dateStr}`));
await waitFor(() => screen.getByRole('dialog'));
expect(screen.queryByTestId('bar-chart')).not.toBeInTheDocument();
});
it('shows bar chart for multiple animals each with their own analysis_summary', async () => {
const dateStr = todayStr();
const summaryA = { total: 18, counts: { Success: 5, Failure: 13 }, success_rate: 0.28 };
const summaryB = { total: 10, counts: { Success: 7, Failure: 3 }, success_rate: 0.7 };
const statuses = [
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }, null, summaryA),
makeStatus('a2000000-0000-0000-0000-000000000002', dateStr, { 'ww000000-0000-0000-0000-000000000099': '310' }, null, summaryB),
];
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />);
fireEvent.click(screen.getByTestId(`day-${dateStr}`));
await waitFor(() => {
expect(within(screen.getByRole('dialog')).getByTestId('bar-chart')).toBeInTheDocument();
});
});
it('excludes from chart animals whose field value is empty on that day, even with analysis_summary', async () => {
const dateStr = todayStr();
const summary = { total: 10, counts: { Success: 7, Failure: 3 }, success_rate: 0.7 };
const statuses = [
// Rat A: has weight AND analysis_summary → appears in chart
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }, null, summary),
// Rat B: has analysis_summary but NO weight → not in day view at all, not in chart
makeStatus('a2000000-0000-0000-0000-000000000002', dateStr, {}, null, summary),
];
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />);
fireEvent.click(screen.getByTestId(`day-${dateStr}`));
await waitFor(() => screen.getByRole('dialog'));
// Chart shows because Rat A has data
expect(screen.getByTestId('bar-chart')).toBeInTheDocument();
// Rat B not shown in day view (no weight value)
const dialog = screen.getByRole('dialog');
expect(within(dialog).queryAllByText('Rat B')).toHaveLength(0);
});
}); });