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:
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user